\documentclass[openany,oneside]{book}
%TC:group tabular 1 1
%TC:group table 0 1
\usepackage{jluthesisUTF8}
%\usepackage{gbt7714}
\usepackage{amsmath}
\usepackage{fontspec}
\usepackage{graphicx}
\usepackage{tikz}
\usepackage{multirow}
\usepackage{lipsum}
\usepackage{graphicx}
\usepackage{tabularx}
\usepackage{listings}
\usepackage{algorithmic}
\usepackage{algorithm}
\usepackage{pgf}
\usepackage{float}
\usepackage{hyperref}
\usepackage{caption}
\usepackage{subcaption}
\usepackage[newfloat]{minted}

\usepackage[backend=bibtex,style=numeric,sorting=none,gbpub=false,bibstyle=gb7714-2015,citestyle=gb7714-2015]{biblatex}
\addbibresource{document.bib}

\newenvironment{code}{\captionsetup{type=listing}}{}
\SetupFloatingEnvironment{listing}{name=清单}
%\usepackage{cite}

\makeatletter
%\newenvironment{breakablealgorithm}
%{% \begin{breakablealgorithm}
%    \begin{center}
%        \refstepcounter{algorithm}% New algorithm
%        \hrule height.8pt depth0pt \kern2pt% \@fs@pre for \@fs@ruled
%        \renewcommand{\caption}[2][\relax]{% Make a new \caption
%            {\raggedright\textbf{\ALG@name~\thealgorithm} ##2\par}%
%            \ifx\relax##1\relax % #1 is \relax
%            \addcontentsline{loa}{algorithm}{\protect\numberline{\thealgorithm}##2}%
%            \else % #1 is not \relax
%            \addcontentsline{loa}{algorithm}{\protect\numberline{\thealgorithm}##1}%
%            \fi
%            \kern2pt\hrule\kern2pt
%        }
%    }{% \end{breakablealgorithm}
%        \kern2pt\hrule\relax% \@fs@post for \@fs@ruled
%    \end{center}
%}
\makeatother

\usetikzlibrary{arrows}
\usepackage{xeCJK}

\floatname{algorithm}{算法}  
\renewcommand{\algorithmicrequire}{\textbf{输入:}}  
\renewcommand{\algorithmicensure}{\textbf{输出:}} 

%opening
\hypersetup{
    pdftitle    = {吉林大学 计算机科学与技术学院 毕业论文},
    pdfsubject  = {基于Spring+Vue的问答网站的研发与后端优化},
    pdfkeywords = {问答网站，Java，Spring Boot，Vue，MySQL，Redis，RabbitMQ，WebSocket},
    pdfauthor   = {何朋蒸}
}



\begin{document}

\frontmatter
\sloppy % 解决中英文混排的断行问题，会加入间距，但不会影响断行 ????

\input{preface/cover}

\pagenumbering{Roman} 
%\pdfbookmark[0]{目~~~~录}{contents}

\tableofcontents
{\xiaosi}
%{\fontsize \fontsize{12.05pt}{14.45pt}\selectfont}
% 清除目录后面空页的页眉和页脚
\clearpage{\pagestyle{empty}\cleardoublepage}

%%% 正文
\mainmatter
\defaultfont                        % 正文使用默认字体，小四，宋体

\chapter{绪论}
\section{研究背景及意义}

\subsection{问答式网站的发展}

问答网站(Q\&A software)是一种采用“用户提问与回答”模式运行的在线服务网站。问答网站通常允许用户根据领域和话题选择性阅读和回答问题，从而使网站社区化。目前已经存在相当多的问答式网站的例子，它们的运作方式各不相同；有的网站专注于一个特定的领域，如\textit{Ask Ubuntu}，\textit{Stack Overflow}；而有的网站则形成了大型社区模式，除了传统的问答服务之外，还提供其他诸如发布文章等形式的内容服务，例如\textit{Quora}和\textit{知乎}等。表\ref{tab:mainstreamqawebsites}\footnote{https://en.wikipedia.org/wiki/Comparison\_of\_Q\%26A\_sites}列举了几个主流问答网站的相关信息。

\begin{table}[H]
	\begin{tabular}{cccc}
		\hline
		\textbf{Website} & \textbf{Founded} & \textbf{Description/focus} & \textbf{Languages available} \\ \hline
		eduyoda          & 2019             & Many topics                & English                      \\
		ask reader       & 2021             & Many topics                & English                      \\ 
		Textanswer       & 2020             & Many topics                & English                      \\
		知乎               & 2011             & Many topics                & Chinese and a few others     \\
		Quora            & 2009             & Many topics                & 22 languages                 \\
		Reddit           & 2005             & All topics                 & Depends on subreddit         \\ \hline
	\end{tabular}
	\caption{部分目前主流或（和）最新的一些问答网站及部分相关信息}
	\label{tab:mainstreamqawebsites}
\end{table}

问答网站的一个公共目的是提供一种允许用户提出问题，并接收这个领域的专家分享回答与知识给其他用户的服务。用户可以从这些问题与回答中学习知识和分享经验，与同一行业和不同行业的人交流学习。

最早在上世纪90年代就出现了早期的在线问答服务。在本世纪00年代出现了几个非常具有影响力的问答网站：\textit{Yahoo! Answers}和\textit{Quora}等。这些网站在传统的问答网站上加入了更多丰富的运营模式，例如用户赚取分数和写作修改问题使其更具意义等。

在2011年，中文互联网出现了\textit{知乎}网站。在2018年年底，知乎官方宣布截止到2018年11月底，知乎的用户数量已经突破了2.2亿，同比增长102\%；同时知乎的问题数量超过了3000万，回答数量超过1.3亿。可见这类大型问答式网站在市场上确实具有一定的需求量，并且需求量逐年增长。

网站的开发和维护是一个长期的过程。在\textit{Quora}和知乎被越来越多人使用的过程中，其网站架构也需要承受越来越大的压力。网站的开发语言，数据存储方式以及服务的部署等诸多环节，都在随着网站的发展逐步演变，最终形成在一段时间内最适合网站的所谓“最佳实践”。除了网站开发的基本技术之外，用户对网站的真实需求和这些需求的实现方式是一个值得研究的话题。例如：如何存储网站的各种信息数据、如何降低网站的响应时间、如何做好数据的备份和错误转移(failover)以及如何设计良好的用户使用界面等。


\subsection{网站开发技术的演变}

网站的开发大约是从上世纪90年代初开始的\cite{scharl2012evolutionary}。网站的开发技术主要经历了最早期仅通过Jakarta Server Pages(JSP)\footnote{https://projects.eclipse.org/projects/ee4j.jsp}或PHP\footnote{https://projects.eclipse.org/projects/ee4j.jsp}来完成，到Model-view-controller(MVC)\cite{deacon2009model}时代，再到Single page application(SPA)和Model–view–viewmodel(MVVM)\cite{bucanek2009model}时代。每一次主流技术的更新换代都是因为久的技术已经逐渐不能适应互联网的快速发展和用户数量日益增长带来的压力。例如：由于JSP技术需要在网页的显示内容中直接写Java代码，当网站业务增大的时候，JSP代码会变得十分臃肿，难以维护；MVC模式将网站开发分层，将前端和后端的开发在一定程度上分开，逻辑全部由后端代码实现，从而降低了网站的开发复杂度；但是由于MVC模式是以后端为主，前端的开发严重依赖于后端，影响了开发效率；得益于Asynchronous JavaScript and XML(Ajax)，前端的自由度得到了极大提高，可以根据需求向后端发送异步请求获取数据，于是浏览器端变得更加丰富，出现了SPA的开发模式，网站的交互性也变得非常强。

回到问答网站的话题，如何为网站设计合理的底层架构、如何选择合适的技术以及如何保证网站的可维护性和可拓展性，这些都是值得思考和探索的问题。本研究正是基于上述背景，将从零开始设计并实现一个问答式网站，并以系统高可用性，高可拓展性和良好简洁的用户接口等为目标，对网站的后端进行一定程度的优化和探索。

\section{研究现状及挑战}

\subsection{搭建网站所需的要素}

在确定需求之后，就需要对需求进行可行性分析。网站的搭建可以大致地分为三个部分：后端、前端和中台。

\subsubsection{网站后端}

后端是项目的核心部分，关心的是网站后台处理数据和业务的逻辑，其实现的结果并不直接被用户所见。后端通常需要连接数据库，对数据进行读取和写入；后端需要管理用户与服务的连接，记录用户的某些状态，从而完成业务逻辑；后端还需要运行服务，即让服务器能跑起来，使得用户可以连接服务器并使用网站提供的服务。后端需要接收前端传来的需求，提供相应的接口以传递数据，从而形成完整的软件产品。因此，后端需要组合使用多项技术，使其能够完成网站的业务逻辑和数据的操作。

目前主流使用的后端编程语言有

\begin{itemize}
	\item Java
	\item PHP
	\item .Net(C\#)
	\item Python
	\item Go
\end{itemize}

 在Stack Overflow 2020年的开发者调查(2020 Developer Survey)\footnote{https://insights.stackoverflow.com/survey/2020\#overview}中显示，目前非脚本语言中，最受欢迎的编程语言是Python，其次是Java、C\#等。这些编程各有优劣，例如，Java历史较长，社区庞大，生态完善，但语言风格较为繁杂，且有一些历史包袱；.Net(C\#)出现相较于Java晚一些，其修正了Java等类C语言的一些毛病，并且提供了强大的运行时环境，但其开源较晚，早先只支持在Windows系统体系下进行开发和使用；Python使用简单，流行度高，但由于其解释性语言的特点，其更适合的应用场景是科学计算和机器学习等算法领域，而非web后端；Go是一门新的语言，I/O性能和并发能力很强大，且节省资源，但其社区还不够庞大，相关的开源库也还不够完善，因此更多地应用与基础设施如容器等的编写，而不是业务代码。
 
 除了编程语言之外，数据库也是后端重要的一部分。在大多数情况下，业务都可以使用关系型数据库\cite{bucanek2009model,schema1995relational,codd1989relational}进行建模。关系型数据库通常使用Structured Query Language(SQL)进行读取，常见的实现有
 
 \begin{itemize}
 	\item \textit{Oracle SQL}
 	\item \textit{MySQL}(free)
 	\item \textit{Microsoft SQL Server}
 	\item \textit{PostgreSQL}
 \end{itemize}

关系型数据库是以关系代数等理论作为基础实现的，它能适用于大多数的业务场景，但在一些情况下，基于key-value形式的非关系型数据库NoSQL(not only SQL)会更加适用。例如通常需要将写热点数据存储在缓存中，而这时候采用基于key-value的存储模式就更适合快速定位到数据。NoSQL除了基于key-value形式的数据库之外，还提供各种各样的存储形式。主流的非关系型数据库的实现有
\begin{itemize}
	\item \textit{Redis}
	\item \textit{MemcacheDB}
	\item \textit{Oracle NoSQL Database}
\end{itemize}
在Stack Overflow 2020年的开发者调查中，最广泛使用的数据库的一项统计中，MySQL排在第一，其次是\textit{PostgreSQL}和Microsoft SQL Server；\textit{MongoDB}排在第五位，Redis排在第六，既最流行的两种非关系型数据库。

NoSQL适合用于非结构化数据的存储，通常使用做网站的缓存数据库。在网站中使用缓存技术是非常有必要的。如果每一次读数据都要从外部磁盘中读取，那么大量的读取和写入操作会导致数据库读取效率十分低下，进一步导致网站性能受到影响，响应不及时。数据库不同数据的读取频率是不一样的，一些数据在一段时间内会被大量反复读取，即为热点数据；而另一些数据则在一定时间内只会被少量的读取，甚至不会被读取。因此，如果将热点数据保存在基于内存的缓存数据库中，那么读取数据不需要每次都去外接磁盘查找，直接在速度很快的内存缓存中读取即可，这样就大幅度提升了网站的响应速度，并且减少了数据库的访问，节省了系统IO资源。在网站中使用缓存的好处主要有：

\begin{itemize}
	\item 提高应用性能——内存的访问速度一般是比磁盘开很多的，因此使用基于内存的缓存可以显著提高网站的整体响应速度，进而提高网站的性能；
	\item 减少数据库开销——一个缓存实例就能提供几十万IOPS(Input/output operations per second)，极大地降低了数据库带来的系统开销，在数据库吞吐率较差的时候尤其明显；
	\item 降低热点数据的查询次数并使得性能可测——拿微博举例，明星的微博访问量一般会远高于普通用户，并且通常访问集中在一段时间内；这种热点数据如果每次访问都读取数据库的话，大量的读取负载会使得应用性能不可预测；同时由于热点数据通常占总数据的一小部分，因此采用缓存来存放一段时间内的热点数据是非常合适的；
\end{itemize}
因此缓存是网站搭建中非常重要的一部分，直接关系到网站的整体性能。

许多网站都会实现一个针对网站自身的消息系统，从而为用户提供分类别的实时消息推送，例如用户的问题被其他用户回答，那么该用户则会收到一条实时消息。这类消息型的数据除了可以用关系型数据库存储，还可以使用更为合适的工具——消息队列(Message broker, message queue)\cite{john2017survey,nguyen2019role,apshankar2002web,mahmoud2004middleware}。

消息队列属于中间件，它将网站中的消息相关的服务独立实现，使得前后端都能使用消息队列的服务。消息队列的用途非常广泛，它通常被用来处理网站中的异步任务。举个例子，用户注册网站账号时，网站通常需要发送邮件确认用户身份，而发送邮件涉及第三方业务，不同邮件服务器处理邮件情况不同，这造成了发送邮件这一过程耗时非常长；在这种情况下，如果网站等待发送邮件完成之后再响应用户，用户会等待非常长的时间，这严重影响用户的体验，网站甚至可能因为这糟糕的使用体验失去这一用户。不仅如此，如果未来因为业务需求，在统一服务中将加入更多的三方业务，如果不使用消息对立，那么网站的响应时间将会越来越长。在使用消息队列之后，可以将这些三方业务异步实现，服务代码只需要将“用户将要注册”这一消息发送给消息队列，发送成功后即可响应用户。而消息的“消费者”监听到队列里的消息之后，即可调用相关的服务去完成这些业务。目前主流成熟的消息队列有

\begin{itemize}
	\item Apache ActiveMQ
	\item Apache Kafka
	\item HiveMQ
	\item Microsoft Azure Service Bus
	\item Oracle Message Broker
	\item RabbitMQ
	\item Redis
\end{itemize}
这些消息队列因为实现语言、功能特性和历史原因，目前应用的场景各不相同。

随着业务的增长，单体项目变得越来越庞大和复杂。例如，对于Java项目，尽管我们可以将项目拆分成多个模块，每次更新模块的时候升级其版本再部署即可。但是我们依然需要修改依赖于这一模块的所有其他模块，而这些模块又需要再次打包发布；每次一个模块的修改，都需要牵动依赖于此模块的所有项目，这种模式带来的测试和部署成本会随着业务的增长越来越大，给项目的整体维护带来压力。单体项目的缺点大致可以归结为

\begin{itemize}
	\item 单体项目的各部分职责变得难以界定；
	\item 单体项目不易做测试——由于庞大的单体项目集成了很多组件和工具，所以单体项目的测试流程会变得很复杂；
	\item 单体项目容易引起依赖冲突——项目中一些功能的实现所需要的依赖容易和其他功能所需的依赖发生冲突，如版本不一致等；
	\item 单体项目的技术选型一旦选定，则整个项目的几乎所有实现都被所选定的技术框架所限定——例如，假设我们选择的是Java语言的一套开发技术，那么我们所有的实现都将使用Java语言来实现。但是很多时候可能有更好的一套技术栈能满足我们的业务需求，而将这些技术栈集成到我们的单体项目则并不容易，集成其他语言或者技术栈也会给开发和测试带来麻烦；
	\item 单体项目的部署成本偏大——我们可能只改动了一个服务的一小部分，但我们仍需打包部署整个项目。
\end{itemize}

最近一些年兴起的微服务架构\cite{nadareishvili2016microservice,cerny2018contextual,balalaie2016microservices}为以上问题提供了一种很好的解决方案。微服务将项目分成一个个耦合度较低的小项目，每个小项目专注于一项服务。每个被分出来的小项目可以独立被开发和部署，这使得每一个服务都可以选择最适合自身业务的技术栈，而不被基础项目的固有技术选型所限制，同时也避免了单体项目中的依赖冲突问题；独立的开发和部署使得项目的更新发布可以更加快速和频繁，而不需要依赖其他服务的更新。

微服务通常是通过网络来实现。各个服务注册到一个注册中心，再配合负载均衡器进行服务的查找，其他服务就能消费负载均衡器所选择的服务实例。由于微服务架构通常是依赖网络协议完成，所以会带来一定程度上的网络负担，除此之外还包括微服务技术自身所带来的迁移和维护成本。但是当项目所需完成的业务足够大，使用单体项目完成业务已经遇到开发和拓展的瓶颈时，采用微服务架构是一个不错的选择。


\subsubsection{网站前端}

网站前端的任务是使用\textit{HTML(HyperText Markup Language)}，\textit{CSS(Cascading Style Sheets)}和\textit{JavaScript}等技术将数据通过图形界面的形式展示给用户。

早期的前端只能通过HTML和CSS将数据展示在页面中，网页是只读的。上世纪90年代中期，由于没有统一的浏览器脚本语言的标准，各家公司开发自己的脚本语言，导致浏览器的实现各不相同，兼容性很差。JavaScript的出现使得前端可以编写逻辑，而不仅仅是展示静态的页面。随后Ajax的出现将前端发展推向一个新的时代，前端可以发送异步请求，从而丰富了网页的交互性。这个时候的动态前端还是通过程序员直接操纵\textit{DOM(Document Object Model)}来实现的，2006年出现的\textit{jQuery}就是一个十分优秀的JavaScript库，它简化了DOM的遍历和修改，也简化了事件处理和Ajax请求。据统计\footnote{https://w3techs.com/technologies/overview/javascript\_library}，到2019年5月，有约77.9\%的网站都使用了jQuery，jQuery一度成为了最流行的JavaScript库。

JavaScript是一个解释性语言，因而是一边解释一边运行，相较于编译型语言，其执行效率比较低，需要的时间比较长。JavaScript引擎的作用就是对JavaScript语言的执行效率就行优化，常见的手段是将其编译成中间代码再用即时编译器进行动态编译，成为本地代码。由谷歌开源的V8\footnote{https://v8.dev/}引擎就是一款十分出色的Javascript引擎。V8引擎首先生成抽象语法树，然后用自己的解释器生成字节码，最后再用自己优化的编译器\textit{TurboFan}将字节码编译成本地机器码。V8并不使用即时编译器执行JavaScript代码而是直接将其编译成优化过的本地机器码，并使用垃圾回收器进行垃圾回收。V8可以将JavaScript编译成x86和ARM架构的64位机的本地代码。支持在浏览器使用或者被其他项目集成，例如\textit{Node.js}的运行时环境就是用的是V8引擎。同时，V8和Node.js还可以让开发人员使用更多灵活的语法糖来编写代码，提升了开发效率。

随后，前端三大框架\textit{Angular.js}、\textit{React.js}以及\textit{Vue.js}陆续诞生，前端开发进入了SPA时代，自己灵活地调取后端数据，自己处理路由和页面的跳转，而不再依赖后端的Controller进行固定的页面跳转控制的编码，由此实现了MVVM模式。后端只需要提供接口，前端和后端约定好接口之后，便可以根据需要实现多套前端工程，灵活多变，也更加工程化。由此，前端开发进入了所谓的“大前端”时代。

前面提高，动态加载页面需要操作DOM，而由程序员自己编码时操作DOM则随意性太高，最终导致DOM操作不合理或者过于频繁，致使页面卡顿。前面提到的Angular、React和Vue等框架，采用了一定的算法对DOM开销进行分析，并用最小的DOM开销完成所需的页面操作，实现相同的效果。

\subsubsection{中台}

中台是最近一些年开始在国内互联网领域被应用的，它并没有一个具体的定义。从功能上可以将中台分成技术中台，业务中台和组织中台等。技术中台最早是由阿里巴巴和京东等一些电商网站实践和总结出来的一套阶段性和战略性的解决方案。例如，电商网站最开始的业务就是简单的买卖交易，但是随着业务的发展，平台加入了更多的业务需求，这些业务都使用了某些服务。如果公司整体架构需要的话，可以把这些服务抽取出来，做成公共的服务平台，然后其他业务调用服务平台的服务，这就形成了一个技术中台。技术中台通常是阶段性的，当目前的发展遇到瓶颈时，可以考虑将业务抽取整合。但目前技术中台成功的领域并不多，最成功的还是电商模式。

\subsection{技术的瓶颈}

一般web项目的性能可以如下方面进行考虑

\begin{enumerate}
	\item \textbf{数据库}——通常包括慢查找语句、写入冲突和较大的join操作占用内存等问题；
	\item \textbf{磁盘}——通常包括随机I/O操作的性能、磁盘碎片问题等；
	\item \textbf{操作系统}——通常包括电量消耗、缓冲区占用等；
	\item \textbf{缓存}——通常包括数据库连续读取、HTTP报头以及浏览器缓等；
	\item \textbf{CPU}——通常包括CPU负载、上下文切换、IO等待和CPU缓存等；
	\item \textbf{网络}——通常包括DNS查找和服务端错误等；
	\item \textbf{程序}——通常包括死锁、事件处理、日志和跟踪器的不足、糟糕的设计、管理客户端状态以及使用的算法复杂度等问题；
	\item \textbf{内存}——通常包括如Java语言的Garbage collection(GC)操作等；
	\item \textbf{流程}——通常包括开发时间、测试时间和部署时间等方面；
\end{enumerate}
其中，数据库是一个重要的性能瓶颈。网站的业务无时无刻不在读写数据库，而数据库对于读写操作需要满足事务的四大特性

\begin{itemize}
	\item \textbf{原子性(Atomicity)}——事务通常是由许多条语句构成的，原子性即保证每一个事务都被当作一个执行单元来对待，一个执行单元要么全部成功，要么全部失败；如果构成事务的任意一条语句在执行的过程中失败了，那么整个事务也将失败，数据库保持原样。原子事务的一个例子是从银行账户A转账到银行账户B。事务分为两个操作，从账户A中取出某个金额和将该金额存入账户B，原子性保证了如果转账失败，则账户A里的金额并不会丢失；
	\item \textbf{一致性(Consistency)}——一致性保证了事务前后数据库从一个有效状态转到另一个有效状态，整个过程维持数据库的不变式：任何数据写入都必须遵循所有的限制规则，如约束条件和触发器等等；
	\item \textbf{隔离性(Isolation)}——事务通常是并发执行的，隔离性保证了并发执行的事务完成后，数据库的状态和这些事务顺序执行的结果一样。隔离性是数据库事务并发控制的主要目标，在隔离性的保障下，未完成的事务对其他事务并不可见；
	\item \textbf{持久性(Durablity)}——持久性保证了当事务被提交(commited)后，即使系统崩溃，数据依然能被保留。
\end{itemize}
其中事务的隔离性通常具有不同的隔离级别。事务的隔离级别越高，则数据越可能被加锁，从而导致事务的并发性减弱。不同的数据库管理系统(Database management system, DBMS)提供不同的事务隔离级别以及实现，但不同实现的共同目标是尽量降低事务的隔离级别，以提高并发性。\textit{ANSI/ISO SQL(American National Standards Institute/International Organization for Standardization)}标准定义了四个事务的隔离级别\cite{berenson2007critique,berenson2007critique}

\begin{enumerate}
	\item \textbf{读未提交(Read uncommitted)}——最低的隔离级别，一个事务可以见其他未提交的事务的引起的改动，这种现象被称为脏读(Dirty reads);
	\item \textbf{读已提交(Read committed)}——DBMS采用基于锁的方式实现并发控制：写锁在事务提交之后再释放，而读锁在\textit{SELECT}语句完成后就被释放。这就导致了不可重复读现象，即在一个读事务中，多次读取同一行数据读到的值可能不同，在读已提交隔离级别，DBMS将提交更新后的值；
	\item \textbf{可重复读(Repeatable reads)}——DBMS仍然采用基于锁的并发控制实现，但是与上一个隔离级别不同的是，在可重复读隔离级别，读锁和写锁都是在事务提交之后再释放。这一隔离级别解决了脏读和不可重读读现象，但是依然存在幻读(Phantom reads)现象。幻读现象是指：由于未对\textit{SELECT ... WHERE}操作加\textit{范围锁(Range locks)}，当一个事务重复一次SELECT ... WHERE语句时，另一个事务在这个范围内插入了新的数据。此时，在可重复读隔离级别，第二次读取将返回不同的数据；
	\item \textbf{可串行化(Serializable)}——最高的事务隔离级别，在这一级别，DBMS采用基于锁的方式实现的并发控制，读锁和写锁都是在事务提交后再释放，当使用\textit{SELECT ... WHERE}语句执行时，会获取范围锁，以此来避免幻读现象。
\end{enumerate}
可见，在保证数据库不出现脏读、不可重复读以及幻读等现象的同时，维持数据库事务的高并发是我们努力的目标。由于众多原因，不同的DBMS的实现采用了不同的事务隔离级别，例如：Oracle只支持读已提交和可串行化两种事务隔离级别，其默认的事务隔离级别为读已提交；而MySQL支持上面描述的四种事务隔离级别，其\textit{InnoDB}\cite{bannon2002innodb,fruhwirt2010innodb}引擎默认的事务隔离级别为可重复读，并采用\textit{Next-Key Lock}机制来避免幻读现象。

不同的数据库或者数据引擎提出了不同的提高并发控制的并发度的方法，例如，MySQL就采用了\textit{MVCC(Multiversion concurrency control)}\cite{carey1986performance,bernstein1983multiversion}方法来控制并发。尽管可以采用一些算法来让较低级别的事务隔离级别更安全，但是问题并没有完全解决，数据库在处理越来越大的数据量时依然难以实现足够的高并发，数据的读和写压力很大，从而成为web项目的瓶颈。另外，根据统计，数据库中读和写的比例通常是不一样的。一般来说数据库中的读操作远比写操作频繁，而写操作通常需要对数据加局部或者全表锁，这会降低数据库的事务并发性能。如果能将数据库的读和写完全分开，那么在这样大量的读取情景下，数据库也能维持相对较高的服务性能。

分布式的数据库架构正好可以解决这个问题。所谓分布式数据库架构，就是将数据库部署在多个服务器上，其中一些服务器上的数据库专门用来写数据，称为\textit{主库(Master)}，另一些服务器上的数据库专门用来读取数据，称为\textit{从库(Slave)}。每个数据库可以部署多个主库和多个从库，通过\textit{主从复制(Master-Slave Replication)}\cite{mazilu2010database,sacha2006gradient,hercule2011study}的机制来保持从库的数据与主库数据的一致；还可以根据业务的需求，将数据分库或分表，即将不相关的数据库表部署在不同的服务器上，从而进一步降低每台数据库服务器上的读写压力，提高应用服务的整体性能。主从复制一般是通过数据库主库的日志记录更新数据库语句的记录，通过网络将日志从主库同步到从库，从库再在本地执行一遍日志里记录的事务，从而使得数据与主库保持一致。不同的业务以及不同的数据量可根据情况合理地设计主从的拓扑结构，以适应自己的业务需求。这样一来，虽然日志的发送会带来一定程度上的网络负担和数据更新的延迟，但是读和写的分离缓解了单台数据库服务器的IO压力，读操作可以通过算法调度被均衡分配到不同的服务器，这样数据库的整体并发度就在无形之中显著提高了。不进如此，分布式的数据库存储还避免了数据库的单点崩溃问题，由于数据库被部署到了多台机器，多台从库相当于主库数据的一个备份，当其中一个节点崩溃时，合理的调度可以避免整个应用程序暂停工作，也能有效地防止数据直接丢失。

\subsection{当前主流技术之间的比较}

当前各种编程语言和开源技术层出不断，各有千秋。技术更新迭代是很快的，但是更新快与稳定似乎又是不能兼得。对于不同的项目，如何选择合适的技术栈很重要，需要综合考虑技术生态，经济能力，开发难度和人力资源等因素，为此需要对当前流行的技术做一个综合比较。

\subsubsection{后端主流技术}
后端的技术大致上可以分为编程语言、开发框架、数据库及数据库框架、缓存以及消息队列等方面。下面分别进行比较分析。

\paragraph{编程语言}

表\ref{tab:mainstreambackendlanguagecomparison}列举了几个主流的后端开发语言的优缺点也主要应用领域。编程语言的比较主要可以从语言流行度、语言学习成本、语言框架生态及其适用领域等方面来进行。

\begin{table}[H]
	\centering
	\resizebox{\textwidth}{!}{
		\begin{tabular}{c|c|c|c}
			\hline
			编程语言       & 优点                                                                                                                                                                                                                                                                       & 缺点                                                                                                                                                                                                                         & 适合领域                                                                        \\ \hline
			Java       & \begin{tabular}[c]{@{}c@{}}Java是最流行的后端编程语言之一，\\ 社区生态丰富，\\ 除了Oracle JDK之外，\\ 还有许多组织和公司\\ 编译的Open JDK版本可以选。\\ Java的语言特性丰富，\\ 能高效地支持许多设计模式，\\ 从而为众多开发框架\\ 的编写提供便利。\\ 由于JVM的广泛应用，\\ Java能如新兴的Kotlin语言\\ 无缝结合使用，\\ 而后者在语言层面\\ 改良了很多Java的固有弊端，\\ 并已成为安卓开发领域新的主力语言\end{tabular} & \begin{tabular}[c]{@{}c@{}}Java在语言层面\\ 存在一些历史遗留的缺点，\\ 例如，Java在语言诞生之初\\ 并未提供泛型的实现，\\ 这就导致其给出的泛型实现\\ 为照顾已有的Java项目\\ 并未能做到真正的运行时泛型，\\ 而是在编译时因为反省擦除而丢失。\\ 此外Java还存在语言啰嗦笨拙，\\ 占用内存较大等问题\end{tabular}                            & \begin{tabular}[c]{@{}c@{}}网站后端、\\ 大数据开发、\\ 安卓等\end{tabular}                \\ \hline
			C\#        & \begin{tabular}[c]{@{}c@{}}C\#继承了C、C++和Java的优点，\\ 提供了更为优秀的垃圾回收实现\\ 以及运行时环境，\\ 并解决了Java的伪泛型问题。\\ .Net框架十分强大，\\ 显著提高了开发人员的开发效率\end{tabular}                                                                                                                               & \begin{tabular}[c]{@{}c@{}}.Net本身十分优秀，\\ 但是其早期绑定Windows平台生态，\\ Microsoft SQL Server、\\ Visual Studio等开发工具\\ 也较为昂贵，\\ 再加上.Net平台开源较晚，\\ 使其在国内一直不够流行，\\ 生态不够大\end{tabular}                                                    & \begin{tabular}[c]{@{}c@{}}Unity游戏开发、\\ 网站后端开发、\\ Windows桌面开发等\end{tabular} \\ \hline
			Python     & \begin{tabular}[c]{@{}c@{}}Python开源并拥有庞大的开发者社区，\\ 拥有大量三方库和优秀的包管理器；\\ Python语法简介，数据结构友好，\\ 并支持面向对象的编程模式\end{tabular}                                                                                                                                                   & \begin{tabular}[c]{@{}c@{}}由于是动态类型的解释性语言，\\ 数据类型可在运行时改变，\\ 这导致其速度相对较慢，\\ 并且不适合应对内存密集型任务。\\ 同样由于Python的动态类型特性，\\ 缺少编译器的编译提醒，\\ Python程序容易出现\\ 运行时的类型不匹配错误，\\ 这对大型应用的开发、\\ 测试和维护都带来了麻烦。\\ 另外，Python对于数据库的连接并不高效\end{tabular} & \begin{tabular}[c]{@{}c@{}}科学计算领域，\\ 人工智能算法领域等\end{tabular}                 \\ \hline
			JavaScript & \begin{tabular}[c]{@{}c@{}}JavaScript同样是解释型语言，\\ 可在运行时进行交互，且语法简洁易学。\\ JavaScript是浏览器端使用最广泛的语言，\\ 在Node.js的支持下也能运用于后端开发，\\ 便于前后端开发语言的统一\end{tabular}                                                                                                                     & \begin{tabular}[c]{@{}c@{}}JavaScript语言运行在浏览器，\\ 可以非常方便地进行运行时的调试和操作，\\ 这也是其容易泄露网站数据，\\ 造成一些安全漏洞从而被用于恶意用途；\\ 另一方面，不同浏览器对JavaScript的\\ 执行效果可能不同，这使得网站为适配不同浏览器\\ 需要做大量兼容性工作\end{tabular}                                      & \begin{tabular}[c]{@{}c@{}}网页前端、\\ 客户端等\end{tabular}                        \\ \hline
			Go         & \begin{tabular}[c]{@{}c@{}}Go语言是编译型语言，无论是编译速度\\ 还是运行速度都非常快，编译后的\\ 程序占用空间很小，并提供垃圾回收机制；\\ Go语言对程序结构的限制非常严苛，\\ 例如，Go语言要求所导入的包必须被使用，\\ 并且必须按照相对顺序导入，\\ 只提供一种循环体——for循环等，\\ 这些限制加快了Go的编译速度，\\ 也将Go语言程序的编写风格尽可能统一。\\ Go采用了更为先进的并发实现，\\ IO性能也十分优秀\end{tabular}                 & \begin{tabular}[c]{@{}c@{}}作为新兴的编程语言，\\ Go语言还有很多不完善的地方，\\ 例如，Go不支持完整的面向对象特性，\\ 不支持泛型、注解和异常处理等程序特性，\\ 导致Go语言的代码可读性会很差，\\ 在处理复杂业务时会看到程序中\\ 大量的本地异常处理。\\ 同时，Go语言可用的开发框架\\ 和包管理模式还不够成熟\end{tabular}                            & \begin{tabular}[c]{@{}c@{}}云服务、\\ 容器等基础设施\end{tabular}                      \\ \hline
		\end{tabular}
	}
	\caption{几种主流网站后端编程语言的优缺点比较}
	\label{tab:mainstreambackendlanguagecomparison}
\end{table}

\paragraph{开发框架}

开发框架即用来简化环境服务配置，管理web服务、web资源以及web接口的软降框架。开发框架的设计目的就是减少开发者重复约定的代码，帮助开发者管理服务资源、管理内存中的对象调用、管理API的映射等。开发框架通常提供一种统一的项目结构和开发模式，在一定程度上规范化开发流程，也促进了社区交流。不同编程语言选择的框架不同，而对于同一编程语言，也有许多可选的开发框架；按照框架的作用，还可以将不同的开发框架进一步细分，如\textit{IoC框架}\footnote{https://en.wikipedia.org/wiki/Inversion\_of\_control}\cite{di2009inversion,wu2006research}、\textit{Web框架}、\textit{ORM框架}\cite{o2008object,chen2016empirical}和安全框架等，这些框架侧重于不同的方面，合在一起才能组成完整的web应用。

对于Java语言，早期流行的\textit{Apache Struts}\footnote{https://struts.apache.org/}框架由于存在很多高风险漏洞，现在已经基本被\textit{Spring\footnote{https://spring.io/projects/spring-framework} MVC}系列淘汰，早期的ORM框架Hibernate目前在国内也逐渐很少开发者使用，更多开发者选择了\textit{MyBatis}\footnote{https://mybatis.org/mybatis-3/}\cite{reddy2017working,reddy2013java}(包括\textit{MyBatis plus}\footnote{https://baomidou.com/guide/})和\textit{Spring Data JPA}\footnote{https://spring.io/projects/spring-data-jpa}。Jetbrains公司做了一些数据统计，如图\ref{fig:javaframeworks}\footnote{https://www.jetbrains.com/zh-cn/lp/devecosystem-2020/java/}所示：

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.75]{./figures/javaframeworks.png}
	\caption{Java框架的使用情况统计}
	\label{fig:javaframeworks}
\end{figure}

\textit{Spring Boot}是为了简化Spring框架所需的繁琐的XML配置而诞生的开源框架，是专门为了简化新Spring项目的搭建和开发过程而设计的。Spring Boot使用自动装配来进行默认Java Bean的配置，如果开发者不需要特殊的配置，则无需再手动配置Java Bean。对于分布式的微服务架构，Spring的子项目Spring Cloud提供了许多便利的工具，包括配置管理、服务查询、负载均衡以及分布式消息等等功能。由图\ref{fig:javaframeworks}可以看出，Spring系列框架已经主导并几乎统一了Java语言生态的开发框架，并在继续蓬勃地发展中。

图\ref{fig:pythonframeworks}\footnote{https://www.jetbrains.com/zh-cn/lp/devecosystem-2020/python/}为2020年Python语言web框架的统计情况，可以看出\textit{Django}与\textit{Flask}两个框架占据了绝对的主导地位。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/pythonframeworks.png}
	\caption{Python框架的使用情况统计}
	\label{fig:pythonframeworks}
\end{figure}

Django相比于Flask更加全面和稳定，提供很多方便的开箱即用的模块；而Flask相对更轻量级，非常易用上手。

对于C\#语言的开发框架，\textit{.Net Core}是最为流行的开源框架。.Net Core是\textit{.Net Framework}的新版本，具有跨平台的能力，具有较强的性能和稳定性。如图\ref{fig:csharpframeworks}\footnote{https://www.jetbrains.com/zh-cn/lp/devecosystem-2020/csharp/}所示。由于.Net Core在2016年左右才开源，导致其在国内相对于Java并没有十分流行起来。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/csharpframeworks.png}
	\caption{C\#框架的使用情况}
	\label{fig:csharpframeworks}
\end{figure}

对于Go语言，最为流行的web框架当属Gin框架，如图\ref{fig:goframeworks}\footnote{https://www.jetbrains.com/zh-cn/lp/devecosystem-2020/go/}所示。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.63]{./figures/goframeworks.png}
	\caption{Go语言的框架使用情况}
	\label{fig:goframeworks}
\end{figure}

此外数据还显示71\%的Go开发者开发微服务，Go语言成为了仅次于Scala的在微服务领域第二受欢迎的语言，这与Go语言自身的特点有很大关系。Go语言采用协程的模式实现并发控制，这使得Go的并发性能非常好，非常适合用于密集型的网络IO和磁盘搜索任务。但由于自身的一些缺陷，Go并不适合用于大型网站的后端搭建。

前端三大框架React、Angular和Vue仍然占据前端开发的主导地位，其中React仍是前端第一框架。图\ref{fig:javascriptframeworks}\footnote{https://www.jetbrains.com/zh-cn/lp/devecosystem-2020/javascript/，其中提到Angular主要由TypeScript开发者使用，使用者占该语言开发者的42\%；而只有17\%的JavaScript开发者使用它}展示了当前主流JavaScript框架的使用情况，其中Express是Node.js提供的web应用框架，主要用于做web后端。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/javascriptframeworks.png}
	\caption{JavaScript框架的使用情况}
	\label{fig:javascriptframeworks}
\end{figure}

ORM框架的主要作用是对面向对象的编程语言中的对象与非面向对象语言中的标量(一般指关系型数据库中的table)做映射，并且通过操作对象的方式来编写简单或者复杂的SQL语句。ORM框架帮助开发者解决了类型兼容问题以及属性之间的一一匹配，节省了开发成本，并能做一些检测和优化功能。对于Java语言，主流的ORM框架除了传统Hibernate之外还有如MyBatis和JPA等
MyBatis除了能完成基本的CRUD(Create, read, update and delete)操作外，还支持数据库的存储过程(Stored procedures)和动态SQL语句，此外还包括分页操作以及一些监控功能。JPA的操作逻辑和Hibernate是一样的，都是通过Hibernate Query Language(HQL)来完成数据库的交互，而不是直接写SQL语句。因此对于使用JPA的项目，可以用较低的成本切换DBMS，例如从MySQL迁移到MongoDB等；相对而言，MyBatis的操作逻辑则是书写SQL语句，这样的结果是，复杂查询语句的书写更加灵活，但是会有依赖于不同SQL实现的问题，从一个DBMS迁移到另一个DBMS，如果二者存在SQL的差异，则需要一定的迁移成本。表\ref{tab:ormframeworksgithub}展示了几个主流Java语言的ORM框架在Github上的star和fork数以及最近一次版本的发布时间的对比，其中MyBatis Plus是MyBatis框架的补充，其对MyBatis做了一系列的优化工作，有效减少了SQL的书写，使得开发效率进一步提高。

\begin{table}[H]
	\centering
	\begin{tabular}{cccc}
		\hline
		框架              & Star  & Fork  & 最近版本的发布时间  \\ \hline
		Mybatis         & 15.6k & 10.6k & 2021-04-26 \\ 
		Spring Data JPA & 2.1k  & 1k    & 2021-04-14 \\ 
		MyBatis Plus    & 11k   & 3k    & 2021-01-15 \\ 
		Hibernate-ORM   & 4.6k  & 3k    & 2016-06-2  \\ \hline
	\end{tabular}
	\caption{几个主流Java语言的ORM框架在Github上的star和fork数以及最\\近一次版本的发布时间的对比}
	\label{tab:ormframeworksgithub}
\end{table}

可见，MyBatis占据了Java语言的ORM层框架的主要地位，MyBatis Plus框架对其进行了补充和优化，且积极更新；Hibernate使用者依然不少，但是已经较长一段时间未发布新的版本了；Spring Data JPA的使用者则暂时相对少一些。

\paragraph{数据库}

数据库主要分为关系型数据库和非关系型数据库。图\ref{fig:databases}\footnote{https://www.jetbrains.com/zh-cn/lp/devecosystem-2020/databases/}展示了当前主流数据库被使用的情况，其中包含关系型数据库和非关系型数据库。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/databases.png}
	\caption{目前主流数据库的使用情况统计}
	\label{fig:databases}
\end{figure}

可以看出，MySQL是被使用最多的数据库，而当前网站开发绝大多数场景都能用关系型数据库进行建模。MongoDB和Redis是使用最多的两个NoSQL数据库，分别主要使用JSON格式和key-value格式进行存储。其中Redis这种使用基于内存并支持分布式和持久化的这种高性能key-value数据库在网站开发中十分适合用来作为热点数据的缓存以及管理用户状态。在Redis官网列出了一些使用了Redis的著名流行网站，如Twitter、GitHub、微博和StackOverdflow等\footnote{https://redis.io/topics/whos-using-redis\#:~:text=A\%20list\%20of\%20well\%20known,Weibo}。

MySQL和PostgreSQL作为被使用最多的两种关系型数据库，两者都有非常庞大的开发者社区，但两者存在着一些差别：

\begin{itemize}
	\item MySQL相对简单，容易被开发者搭建和管理；
	\item PostregSQL功能更加丰富，适用于处理复杂查询和处理大型数据库；
	\item PostgreSQL支持面向对象功能，即数据库表之间支持继承和函数重载；
	\item PostgreSQL支持的编程语言丰富；
	\item 两者管理索引的方式不同；
	\item MySQL语法相对宽松，且没有大小写限制；PostgreSQL限制大小写规范，不支持UTF-8语法；
	\item PostgreSQL支持NoSQL和许多类型的数据格式；
	\item MySQL的云支持更好；
	\item MySQL处理只读命令是更快速，
\end{itemize}
可见，二者皆有优点，对于相对简单的数据库MySQL表现得十分快速和稳定，而对于大型数据库，PostgreSQL更快更强。

关系型数据库存在很多的限制。首先，关系型数据库难以横向扩展。当开发者向当前的单点数据库或者数据库集群中加入更多的机器时，要保证数据库的数据一致性就会变得困难起来，因为客户端往数据库集群中的一个节点写数据时，其他节点可能还没有及时更新数据。另一个限制就是，关系型数据库存储的是结构化的数据，或者是预先定义的数据类型。而随着业务的复杂和多样化，越来越多常见的数据其实是非结构化的，例如邮件、消息、图片和视频等等。因此，非关系型数据库的应用是非常必要的。表\ref{tab:nosqldatabases}列举了几种常见的非关系型数据库的数据格式及其对应的可选数据库产品。

\begin{table}[H]
	\centering
	\begin{tabular}{cc}
		\hline
		数据存储模型 & 可选数据库                   \\ \hline
		键值对    & Redis, MemcacheDB       \\ 
		列式数据库  & Cassandra, Apache HBase \\ 
		文档类型   & MongoDB, Couchbase      \\ 
		图数据库   & OrientDB, Neo4j         \\ \hline
	\end{tabular}
	\caption{几种常见的非关系型数据库的数据格式及可选数据库产品}
	\label{tab:nosqldatabases}
\end{table}

NoSQL数据库尽力使得数据保持最终一致性。所谓最终一致性，就是指新写入数据库的数据在数据库的其他节点中尽可能地保持一致，尽管不能立即将数据同步到其他节点，但是能保证数据库返回的是之前某个版本的数据，而非返回错误。此外，非结构化或者半结构化的数据用NoSQL存储还能获得更多灵活的功能。

\paragraph{缓存}

前面已经阐述了缓存在网站中的重要性。Redis和Memcached是两款流行的开源的基于内存的缓存数据库。两者都是简单易用并且性能优秀，但仍有一些差异，例如Redis提供了很多丰富的功能，使其拥有大量的使用场景；而Memcached则设计相对简单一些，如表\ref{tab:redisvsmemcached}\footnote{https://aws.amazon.com/elasticache/redis-vs-memcached/}所示。

\begin{table}[H]
	\centering
	\begin{tabular}{ccc}
		\hline
		功能                                               & Memcached & Redis \\ \hline
		Sub-millisecond latency                          & Yes       & Yes   \\ 
		Developer ease of use                            & Yes       & Yes   \\ 
		Data partitioning                                & Yes       & Yes   \\ 
		Support for a broad set of programming languages & Yes       & Yes   \\
		Advanced data structures                         & -         & Yes   \\
		Multithreaded architecture                       & Yes       & -     \\ 
		Snapshots                                        & -         & Yes   \\ 
		Replication                                      & -         & Yes   \\
		Transactions                                     & -         & Yes   \\ 
		Pub/Sub                                          & -         & Yes   \\ 
		Lua scripting                                    & -         & Yes   \\ 
		Geospatial support                               & -         & Yes   \\ \hline
	\end{tabular}
	\caption{Memcached与Redis的比较}
	\label{tab:redisvsmemcached}
\end{table}

可见，Redis功能丰富，多才多艺，这也是Redis被使用更多的重要原因。
% TODO: REDIS V.S. MEMCACHE


\paragraph{消息队列}

前面提到了消息队列的主要用于项目中的异步任务或者用于微服务通信。目前主流的几款消息队列产品的比较如表\ref{tab:messagebrokers}\footnote{https://ultimate-comparisons.github.io/ultimate-message-broker-comparison/}所示。

\begin{table}[H]
	\centering
	\resizebox{\textwidth}{!}{
		\begin{tabular}{ccccccc}
			\hline
			消息队列            & 编写语言   & 支持协议                                                                            & 支持平台                                                           & JMS支持 & Broker Backend & Google查询次数 \\ \hline
			Apache ActiveMQ & Java   & \begin{tabular}[c]{@{}c@{}}AMQP, AUTO, \\ MQTT, OpenWire, \\ STOMP\end{tabular} & \begin{tabular}[c]{@{}c@{}}Linux, \\ Windows\end{tabular}      & Yes   & None           & 1390000    \\ 
			Apache Kafka    & Scala  &                                                                                 & \begin{tabular}[c]{@{}c@{}}Linux, OSX, \\ Windows\end{tabular} & No    & None           & 562.000    \\ 
			RabbitMQ        & Erlang & \begin{tabular}[c]{@{}c@{}}AMQP, MQTT, \\ STOMP\end{tabular}                    & \begin{tabular}[c]{@{}c@{}}Linux, OSX, \\ Windows\end{tabular} & Yes   & None           & 2.030.000  \\ 
			Redis           & C      & RESP                                                                            & Linux, OSX                                                     & No    & None           & 18.400.000 \\ \hline
		\end{tabular}
	}
	\caption{几个主流消息队列的对比，其中Kafka支持的协议是其自定义的一\\套基于TCP的二进制协议}
	\label{tab:messagebrokers}
\end{table}

其中ActiveMQ是目前最流行的一款强大的开源消息队列服务，同时支持AMQP、AUTO、MQTT、OpenWire和STOMP协议，并同时支持Linux和Windows平台；而RabbitMQ作为ActiveMQ的下一代消息对立，同样功能丰富，并同时支持AMQP、MQTT以及STOMP协议，但是由于RabitMQ是由于\textit{Erlang}语言编写的，其他语言的开发人员对Erlang语言并不熟悉，所以许多开发者更倾向于使用Apache Kafka(使用基于JVM的Scala语言编写)。图\ref{fig:messagebrokerscomparison}\footnote{https://www.g2.com/compare/apache-activemq-vs-apache-kafka-vs-rabbitmq-vs-redis}展示了不同体量公司对这几款消息队列使用情况。
\begin{figure}[H]
	\centering
	\includegraphics[scale=0.45]{./figures/messagebrokerscomparison.png}
	\caption{不同体量公司对这几款消息队列使用情况的一个统计，从左到\\右依次是ActiveMQ,、Kafka、RabbitMQ以及Redis}
	\label{fig:messagebrokerscomparison}
\end{figure}

可以看出，对于体量和业务较小的公司，更倾向于选择使用Redis和RabbitMQ作为消息队列；而大型企业更倾向于选择容量更大的Kafka，其次是ActiveMQ。


\subsubsection{前端主流技术}

前面提到，前端有三大开发框架，分别是React、Angular以及Vue。其中React由\textit{Facebook}公司开发并首次发布于2013年、Angular由\textit{Google}公司开发，并首次发布于2010年，而Vue则由Google曾经的开发者尤雨溪开发，首次发布于2014年。图\ref{fig:npmtrends}\footnote{https://www.npmtrends.com/angular-vs-react-vs-vue}展示了这三大框架最近两年的下载趋势，可以看出React的下载次数远远大于另外两个框架的下载次数，其受欢迎程度高居前端框架第一位。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.3]{./figures/npmtrends.png}
	\caption{React、Angular以及Vue近两年的npm下载趋势}
	\label{fig:npmtrends}
\end{figure}


图\ref{fig:frontframeworksgithub}\footnote{https://www.npmtrends.com/angular-vs-react-vs-vue}展示了这三个框架在Github上的star数量对比，图\ref{fig:frontframeworksgithubtrends}\footnote{https://www.codeinwp.com/blog/angular-vs-vue-vs-react/}则展示了这三大框架在Github上的star数量变化趋势的对比。可以看出，Vue作为后起之秀，依然可以有着不输于甚至高于前端第一大框架React框架的人气，这得益于Vue简单易学的特点。

\begin{figure}[H]
	\centering
	\begin{subfigure}[b]{0.9\textwidth}
		\centering
		\includegraphics[width=\textwidth]{./figures/frontframeworksgithubdata.png}
		\caption{React、Angular以及Vue三个框架在GitHub上的Star数对比}
		\label{fig:frontframeworksgithub}
	\end{subfigure}
	~
	\begin{subfigure}[b]{0.9\textwidth}
		\centering
		\includegraphics[width=\textwidth]{./figures/frontframeworksgithub.png}
		\caption{React、Angular以及Vue三个框架在GitHub上的Star数变化趋势对比}
		\label{fig:frontframeworksgithubtrends}
	\end{subfigure}
	\caption{React、Angular以及Vue的GitHub部分数据对比}
\end{figure}


\section{研究内容}


经过上述背景介绍以及对当前网站开发基础技术环节的流行技术作出的对比，本研究将通过设计并实现一个特定的问答式网站，并对其后端作出一定的优化与分析，在此过程中对网站后端的设计与开发过程进行探索和总结，总结出网站后端开发的一套相对较好的实践方法。为此，本研究将

\begin{enumerate}
	\item 分析问答类网站的基础需求，设定研究目标；
	\item 对所得到的研究目标进行可行性分析，并在此基础上作出技术选型；
	\item 根据所定的研究目标和自身开发经验，对目标应用进行初始架构的设计与分析；
	\item 对目标项目进行数据建模，得到适合目标应用的数据模型；
	\item 对目标应用进行代码设计，选择合理的代码层次与结构；
	\item 对目标应用的后端接口进行分析和设计；
	\item 对项目进行编码；
	\item 针对目标项目的特点，制定相应的优化策略，并加以实现；
	\item 对目标项目的完成情况进行分析与评估，
\end{enumerate}
此外，还将对目标项目未来的发展作出计划，并分析其可行性。


\chapter{项目分析与设计}

本研究的目标项目是一个问答式网站，取名Takkyu\footnote{名称来源于日语桌球(takkyuu)}。本章将从需求分析开始，确定项目的初始目标，选择合适的技术栈，再对项目进行数据建模、代码设计、接口设计以及后端优化等工作，最后对项目进行一定的测试与评估，并对其未来的发展进行规划。

\section{需求分析与目标}

搭建网站第一步是确定需求。需求通常是很难在一开始完全确定的，但是本研究的关注点并不是复杂多变的软件需求，而是探索研究对于基本需求的详细实现过程和优化，因此本研究依然采用传统的瀑布模型作为软件开发模型。瀑布模型是一种传统的软件开发模型，它将软件开发分成线性序列化的各个阶段，每个阶段专注于一项任务，并强烈依赖于该阶段之前的阶段。瀑布模型将软件开发分成了如下的阶段顺序

\begin{enumerate}
	\item 系统和软件需求；
	\item 系统分析；
	\item 系统设计：一般包括概要设计和详细设计；
	\item 编码；
	\item 测试；
	\item 运行和维护，
\end{enumerate}
每一个阶段都依赖前一阶段的完成，而每一节点如果发现问题，则需要回到前面的阶段进行处理。

Quora是一个结合问答式平台与社交平台的网站，除了提供基础的问答功能外，还能进一步将具有相似兴趣的用户聚集在一定的社区空间，并进行知识分享与信息交流。Quora提供了以下主要功能：

\begin{enumerate}
	\item 用户提问；
	\item 关注问题——用户可以通过关注问题订阅问题动向，当问题有新回答时可以收到消息通知；
	\item 管理话题——Quora用户可以选择自己感兴趣的话题，可以即使收到该话题的热门问答的推荐；
	\item 用户回答问题；
	\item 回答问题日志；
	\item 重定向到问题和回答；
	\item 修改问题和回答——在提出问题或者提交回答之后，依然可以对问题和回答进一步编辑；
	\item 评论回答与回复评论——用户可以对已有的回答进行评论，而回答的评论依然可以被回复；
	\item 消息通知系统——将消息通知分类推送，例如系统通知，问题被回答等；
	\item 发布文章，
\end{enumerate}

一个网站的功能并不是一成不变的，随着网站的发展和业务的变化，经常会由添加功能与砍掉功能的时候；问答网站的核心功能是用户提问与回答问题，在此基础上对提高问题与回答的质量，优化用户的体验是网站进一步的目标；Takkyu的初始需求可以总结如下：

\begin{enumerate}
	\item \textbf{用户注册与登录}——用户可以使用电子邮箱地址进行注册，并用于身份验证；用户注册之后可以随时登录账号；
	\item \textbf{用户管理账户信息}——用户的账户信息包括如电话号码、身份信息和邮箱地址等；
	\item \textbf{用户提问与编辑问题}——用户可以随时发布一个新的问题，当用户发布问题之后，其他用户便可以查看此问题，并根据情况给出回答；此外，提问者可以在提出问题之后对问题进行编辑更新；
	\item \textbf{用户回答问题}——当其他用户发布了新的问题之后，用户可以选择回答此问题；用户发布回答之后依然可以对回答进行编辑；当用户回答问题之后，该问题的提问者将收到消息通知；
	\item  \textbf{用户评论已有的回答}——用户可以对回答进行评论；当用户提交评论之后被评论的回答的作者应当收到消息通知；
	\item \textbf{用户回复其他评论}——用户可以回复回答的评论以及其他用户的评论；被回复的用户将收到消息通知；
	\item \textbf{消息通知系统}——用户将收到诸如系统通知、问题被回复等即时消息的通知；
	\item \textbf{用户修改密码}；
	\item \textbf{用户搜索问题与答案}，
\end{enumerate}

\section{可行性分析}

在需求确定之后，本研究将对此需求的可行性进行分析(Feasibility analysis)。可行性分析通常需要考虑如下几个方面：

\begin{itemize}
	\item \textit{技术可行性}——讨论需求是否可以由当前的技术实现；
	\item \textit{操作可行性}——是否有人进行部署和操作；
	\item \textit{合法可行性}——项目需求是否符合法律法规；
	\item \textit{调度可行性}——是否能一次性调度；
	\item \textit{经济可行性}——讨论项目可获得的利润与所需付出的成本关系；
\end{itemize}

\paragraph{技术可行性}

在绪论中，本研究讨论了当前搭建网站的基本要素以及当前流行技术的使用现状。当前搭建网站从编程语言、开发框架、数据库管理系统以及服务器等已有众多较为完善的的开源技术可用，因此，本研究的目标项目在技术上是完全可行的。

\paragraph{操作可行性}

本研究将对该项目进行搭建与操作。

\paragraph{合法可行性}

本项目并不涉及任何违法技术与服务。

\paragraph{调度可行性}

本项目当前是在给定的需求上进行开发，且并无其他临时需求，因此本研究的目标项目是可以一次性调度的。

\paragraph{经济可行性}

本研究的研究目的是探讨问答式网站的设计与搭建的基本方面，并总结出一套合理并可行的实践方法，并为计划将其部于公网服务器；此外，由于本项目所采用的各项技术都有流行且完备的开源免费技术可选，因此本项目在并无经济问题。

\section{设计架构}

本研究的架构设计分主要分为三部分：用例(Use case)设计、后端架构以及前端架构三部分。

\subsection{用例设计}

用例设计即结合项目需求，设计用户在使用网站时将遇到的场景。Takkyu的用例可以总结如下：

\paragraph{用户注册}

用户填写用户名(必填)、邮箱地址(必填)、登录密码(必填)、性别(可选)以及电话号码(可选，因为本项目并不使用电话号码相关服务)，点击获取邮箱验证码：

\begin{itemize}
	\item 若成功，则系统将向用户邮箱发送验证码，用户填入验证码，即可点击登录，进入子用例：
	\begin{itemize}
		\item 若成功，用户注册成功，系统跳转至登录页面；
		\item 若失败，提示用户失败原因；
	\end{itemize}
	\item 若失败，则停留在当前页面，并反馈给用户失败原因，例如表单填写不合要求或其他原因等。
\end{itemize}

\paragraph{用户登录}

用户登录的前提是用户已经注册了账号；用户填写用户名或者邮箱地址以及账号密码，点击登录：
\begin{itemize}
	\item 若成功，则跳转至网站主页；
	\item 若失败，提示用户错误原因，如表单问题或者网络问题等，用户可选择重试操作。
\end{itemize}

\paragraph{用户修改用户信息}

这里的用户信息包括用户邮箱、电话号码以及用户性别。用户查看账户信息，每一项信息提供修改功能，用户可以为每一项单独进行修改和提交：

\begin{itemize}
	\item 若成功，提示用户修改信息成功，并更新用户信息。若用户修改的信息为邮箱地址，需验证身份。用户点击获取验证码，系统将向当前邮箱发送验证码，以确认用户身份，进入子用例：
	\begin{itemize}
		\item 若验证码发送成功且用户输入正确的验证码和新的邮箱地址，则修改成功，系统向用户新邮箱发送通知邮件；
		\item 若失败，则提示用户错误信息，用户可以选择重试；
	\end{itemize}
	\item 若失败，提示用户错误原因。
\end{itemize}

\paragraph{用户修改密码}

用户可以在设置界面设置用户的登录密码，用户输入新的用户密码并进行确认，点击发送验证码，系统将向用户邮箱发送验证码，用户输入验证码并提交：

\begin{itemize}
	\item 若成功，则用户更新密码成功，系统将用户退出登录，并将用户重定向到登录页面，用户需重新登录；
	\item 若失败，则提示用户失败原因，用户可选择重试。
\end{itemize}

\paragraph{用户提问}

用户点击提问可进行添加问题，用户编辑问题标题(必填)和问题详细(必填)，点击提交即可提交问题：

\begin{itemize}
	\item 若成功，则提示用户问题添加成功，将用户重定向到问题详情页；
	\item 若失败，则提示用户失败原因。
\end{itemize}

\paragraph{用户回答问题}

用户在问题列表旁边点击回答问题，即可填写回答，点击提交即可提交回答：

\begin{itemize}
	\item 若成功，则提示用户回答成功；
	\item 若失败，则提示用户错误信息；
\end{itemize}

\paragraph{用户评论问答}

用户评论回答功能包括

\begin{enumerate}
	\item 对用户的回答进行补充，或对该回答进行反驳；
	\item 对该回答进行评价于交流，
\end{enumerate}
用户在每一条回答出可点击添加评论，用户填写评论并点击提交:

\begin{itemize}
	\item 若成功，则提示用户评论成功；
	\item 若失败，则提示用户错误信息。
\end{itemize}

\paragraph{用户在评论中回复}

用户在评论中可以回复该条评论，也可以回复该评论中其他的回复。用户在需要评论的留言下点击添加回复，输入回复内容，点击提交即可：

\begin{itemize}
	\item 若成功，则显示评论，并提示用户添加成功；
	\item 若失败，则提示用户错误信息。
\end{itemize}

\paragraph{用户查看自己的提交}

用户自己的提交包括自己提的所有问题以及所有的回答。用户进入个人主页，在可在全部问题于全部回答两个tab间切换查看。用户可点击问题详情或回答详情，并跳转到详情页。

\paragraph{用户查看通知}

用户可以根据提示查看收到的即时通知，用户点击查看通知即可跳转到通知详情页，可根据分类查看不同类别的通知。

\paragraph{用户搜索}

用户可以在搜索框中输入要搜索的内容，即可进行搜索操作：

\begin{itemize}
	\item 若成功，则可以查看搜索到的内容，并通过点击跳转到详情页；
	\item 若失败，则提示用户错误信息。
\end{itemize}

\paragraph{用户退出登录}

用户点击退出登录即可退出登录，若成功则将用户跳转到登录页面，用户可以选择重新登录或离去。

以上即为Takkyu的所有用例。

\subsection{架构设计}

阿里巴巴前技术专家李智慧在其所著的《大型网站技术架构：核心技术与原理》\cite{李智慧2013大型网站技术架构}一书中列举了几个网站架构设计的误区：

\begin{enumerate}
	\item 一味追随大公司的解决方案；
	\item 为了技术而技术；
	\item 企图用技术解决所有问题
\end{enumerate}

其中第一点是警告我们，不能只看到大公司的光环效应，而忽略了其技术底蕴和实际情况，从而盲目模仿；第二点是强调，网站的技术应该是服务于业务，不然就失去了其意义，因而在做技术选择的时候，应该重点考虑的是业务发展的实际需要，而非一味追求最新最时髦的技术，否则将可能导致未来的架构之路变得更加艰难；第三点是提醒我们，有时候真正问题并非技术架构，而在于业务架构，也就说我们应该合理设计业务，而非把所有责任都抛给技术。

李智慧提到的这三点架构设计误区令人深省。没有那个网站从一开始就是大型网站，一开始就拥有大量的用户、高量的并发访问以及海量的数据。Takkyu在设计时，一方面业务并不复杂，另一方面本研究技术实力也并非足够深厚，因此本研究并非把其当成一个大型网站来对待。但这不意味着本研究只要求Takkyu能够运行就足够了，本研究依然会对其后端架构作出一定的优化，使其在初始架构下就能在一定程度上拥有一些容错性和较高的可用性，具体优化过程将在实现和优化部分叙述。在这里我们先给出Takkyu的基本架构，如图\ref{fig:initialarchitecture}所示。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.5]{./figures/initialarchitecture.png}
	\caption{Takkyu的初始架构}
	\label{fig:initialarchitecture}
\end{figure}

用户与网站前端进行交互，前端发送同步或异步请求给后端，后弹根据前端发送的请求的类型以及可能携带的数据进行处理。后端处理的过程中可能涉及到查找数据缓存，若缓存不命中，可查找数据库等，由此将处理结果返回给前端。前端根据后端返回的处理结果以及数据，将其展示给用户。在这里的架构图中，前端是和后端分开开发和部署的，分离前后端的好处在绪论中已经叙述，也有利于未来可能添加的其他的独立前端与后端进行交互，如图\ref{fig:addnewfrontend}所示。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.5]{./figures/addnewfrontend.png}
	\caption{加入新的前端之后的架构}
	\label{fig:addnewfrontend}
\end{figure}

\section{技术选型}

在确定用例之后，则可进一步根据需求和用例，对所需用到的技术进行分析和选择，从而选出适合搭建Takkyu项目的技术栈。需要选择的技术包括编程语言、开发框架、数据库、缓存、消息队列以及其他中间件和服务等。

\subsection{编程语言的选择}

前面提到，目前主流的web服务端开发语言主要有Java、PHP、Python、.Net(C\#)以及Go等。Quora的首席执行官Adam D'Angelo在一个关于为什么Quora要使用Python作为主要开发语言的提问\footnote{https://www.quora.com/Why-did-Quora-choose-Python-for-its-development-What-technological-challenges-did-the-founders-face-before-they-decided-to-go-with-Python-rather-than-PHP}下做出了他的回答。他首先从Facebook的经验上看，觉得PHP存在很多历史遗留问题，没有选择PHP；随后他提到对于了C\#、Java和Scala(一种运行在\textit{Java虚拟机(Java virtual machine, JVM)}\footnote{https://docs.oracle.com/javase/specs/index.html}\cite{venners1998java,stark2012java}上的语言)的比较：

\begin{quote}
	C\# seemed pretty promising. As a programming language, it's great, but:
	\begin{itemize}
		\item We didn't want to be on the Microsoft stack. We were up for learning something new, and MS SQL Server actually seemed pretty good, but we knew we'd need to integrate with lots of open source code that has only second-class support for .NET, if it supports it at all. Also, most of the best engineers these days are used to open source stuff.
		\item We didn't want to take the risk of being on Mono (an open source implementation of C\#/.NET). It's not clear how long funding will be around for that project, and I'd heard of various performance problems. Plus, it seemed like everything else in the C\# ecosystem would assume we were on the Microsoft stack.
	\end{itemize}

	For a lot of little reasons, Java programs end up being longer and more painful to write than the equivalent Python programs. It's also harder to interoperate with non-Java stuff. Scala had a lot of the downsides of Java and the JVM, although it wasn't quite as bad. The language seemed a little too new and like it would bring some unnecessary risk (for example, who knows how good support will be in 10 years).
\end{quote}
总结起来就是尽管C\#是一个不错的选择，但是他不想使用微软的一套技术栈，因为开发过程中肯定会集成许多开源技术，而许多开源技术对.Net的支持不够好；另一方面，Java语言和其他非Java语言的开发工具的集成相对Python更复杂，而Scala又存在许多Java和JVM上的缺点，以及语言不够成熟带来的潜在风险。而Python语言的主要特点是速度相对于其他编译型语言来说比较慢，另一方面由于是动态类型语言，它缺少类型检查。但是对于Adam来说，他和他的团队对Python非常熟悉，另一方面，对于大部分场景Python都能满足他们的业务需求，因为他们将对于性能要求较高的部分代码尽量用C++语言来编写。对于类型检查，则使用大量的单元测试来确保类型不会出错。最后Adam提到团队的早期员工都愿意迁移到Python语言，并且Python的生态有向好的方向发展的趋势，这加强了他们选择Python生态的信心。

从Quora的例子来看，选择编程语言要结合语言生态，业务需要和开发人员的技术基础。前面已经对当前主流的后端开发语言的优缺点和适用领域进行过讨论，本研究选择使用Java语言作为开发语言。一方面是因为Java语言功能丰富，框架生态完备，社区庞大，适合用于网站后端的开发；另一方面是因为本研究对Java语言更加熟悉，从而节省了其他语言和框架一定程度上的学习成本。

本研究使用JDK 11作为开发工具，JDK 11是于2018年发布的最近的一个长期维护的Java版本，其在JDK 8的基础上提供了一些功能的补充，并且保持较高的稳定性。

\subsection{开发框架的选择}

选定后端语言之后，我们需要选定一个强大的开发框架。开发框架可以为项目的搭建与开发完成很多繁琐的配置，并提供很多开箱即用的功能，极大地提高了开发效率，也减少重复“造轮子”，从而降低了基础代码出现错误的几率。前面提到，将前端和后端分离开发能够有效减少技术的耦合，使得前端开发不依赖于后端，具有较高的灵活性。因此，Takkyu项目的开发也将采用前后端分离开发的形式。为此，本研究需要选定后端和前端的开发框架。

\subsubsection{后端开发框架}

在绪论中，本研究对Java语言的开发框架做了一个比较，数据显示，Spring系列框架Spring Boot以及Spring MVC几乎统一了Java语言的框架生态，其提供了稳定的对象容器，帮助开发者管理Java虚拟机中的Java对象；Spring Boot的内嵌Tomcat服务器和自动装配能力，为开发者省去了配置Tomcat和一些基本工具的配置；此外，Spring框架社区庞大，这使得开发中的各种问题能够即时得到社区帮助。因此Takkyu项目也将使用 Spring Boot作为后端框架。由于Takkyu暂时业务并不复杂，本研究中暂时并不考虑将其搭建成微服务架构，因此本研究并未选用Spring Cloud框架。

\subsubsection{前端开发框架}

在绪论中，本研究比较了前端三大框架的流行程度和受欢迎程度。React框架目前仍为最流行的前端开发框架，而喜爱Vue的开发者似乎相对来说更多一些。这两者都是快速便捷的JavaScript库，都使用虚拟DOM并用有独立的路由和状态管理器，但是两者之间也存在较大的区别。比较突出的差别是，二者的模板渲染方式不同——React是通过JavaScript实现模板的常见语法；而Vue是通过单独的模板指令来完成。另一个重要的差别是，二者的渲染过程不同——Vue的本质是一种MVVM框架，而不是前端组件化框架。这些设计上的不同还导致一个重要的事实：Vue更加易学易上手。由于本研究相对来说对Vue框架更为熟悉，因此本研究选择Vue作为前端开发框架。

\subsection{关系数据库的选择}

在绪论部分，本研究对比了当前最流行的两个关系数据库MySQL和PostgreSQL。PostgreSQL提供了更丰富的功能，支持数据库表的面向对象以及更多的编程语言集成，适合用户管理大型的复杂的数据库；而MySQL相对来说更加简单，易于搭建和管理，云支持更好且处理只读命令更加快捷，拥有良好的事务实现。考虑到Takkyu项目目前的数据库规模和数据库操作逻辑并不会太大和太复杂，并且考虑到项目中读取操作远大于写操作的特点，因此相对简单的MySQL更加适合Takkyu项目。此外，关于数据库于Java程序集成所使用的ORM框架将在项目的实现部分叙述。

\subsection{缓存数据库的选择}

在绪论中，本研究对当前流行的两个基于内存的缓存数据库做了初步的比较，可见相比之下Redis功能更加丰富，支持诸如Pub/Sub、快照以及主从复制等Memcached不支持的重要功能，并且Redis简单易用，高效快捷。因此本研究采用Redis作为Takkyu项目的缓存数据库。

\subsection{消息队列的选择}

本研究在绪论中对比了目前主流的几款消息队列的所用的编程语言、所支持的协议以及所支持的平台等信息。考虑到Takkyu的业务消息量暂时不会十分巨大，RabbitMQ提供的支持足够；并且由于RabbitMQ正好支持STOMP协议，更加有利于项目中消息系统的集成与开发。因此本研究选用了RabbitMQ作为消息队列。与STOMP协议的集成将在项目的实现部分叙述。
%TODO: MAY ADD ELASTIC SEARCH TO THIS PART IN THE FUTURE


\section{数据建模}

图\ref{fig:usertable}至\ref{fig:answertable}展示了Takkyu项目基本的关系数据库表。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.85]{./figures/usertable.png}
	\caption{User表，表示用户}
	\label{fig:usertable}
\end{figure}


\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/roletable.png}
	\caption{Role表，表示用户身份}
	\label{fig:roletable}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/user_roletable.png}
	\caption{User\_Role表，表示用户与身份之间的对应关系}
	\label{fig:user_roletable}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/questiontable.png}
	\caption{Question表，表示用户的问题}
	\label{fig:questiontable}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/commenttable.png}
	\caption{Comment表，表示用户的评论}
	\label{fig:commenttable}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/replytable.png}
	\caption{Reply表，表示用户的回复}
	\label{fig:replytable}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/answertable.png}
	\caption{Answer表，表示用户的回答}
	\label{fig:answertable}
\end{figure}

其中\textit{PK}即为数据库表的主键，我们在user表中加入了两个\textit{唯一索引(Unique key)}分别为\textit{username}和\textit{email\_address}，用来应对大量注册和登录场景时的检索要求，唯一索引还保证这两个字段都是不可重复的，即用户名和邮箱地址都是唯一的，使用一个邮箱也只能注册一个账号。User表中保存了用户的基本信息，包括用户名、用户邮箱、登录密码以及一个布尔类型的字段指示该用户是否为管理员，user\_id为用户表中的主键，唯一标识每个用户；Role表表示用户的身份，例如普通的"USER\_ROLE"或者管理员"ADMIN\_ROLE"。User\_Role表存储用户的身份，是一张关系表。Question表存储用户的问题，其中包括问题的标题、问题的主体、提问人、创建时间和修改时间等； Answer表存放问题的回答，包括回答主体、回答者、回答时间、更新时间等；Comment表存放回答的评论，包括评论内容、评论人以及所评论的回答等；Reply表存放回答评论下的回复，包括回复的内容、所回复的评论、所回复的回复以及回复人等。

以上即为Takkyu项目的初始数据建模，在业务的发展过程中，还可能加入登录记录统计等等其他数据模型。

\section{代码设计}

这里的代码设计分为后端与前端的代码设计，主要包括代码功能分层和代码文件结构的组织等内容。

\subsection{后端代码的设计}

后端代码按照功能性可大致分为基础配置与环境搭建、数据实体与数据交互、业务逻辑实现、web请求处理以及其他功能代码层。

项目的基础配置包括配置数据库、Redis、消息队列、模版引擎、邮件服务等配置；Web应用需要知道数据库、Redis以及消息队列等服务所运行的的IP地址和端口才能与之连接，同样对与邮件服务，web应用也需要知道其服务器域名和账号信息才能使用等；

数据实体与数据交互层则为数据库表和数据库操作语句在web项目中的表示，例如，对于关系数据库中的每一个数据库表，web应用中通常需要一个与之对应的Java类，这样就将数据库的逻辑操作等转化为Java语言面向对象的操作模式。此外，数据交互部分通常结合ORM框架来完成，将简单或复杂的SQL语句与Java函数对应起来，从而方便程序开发；

业务逻辑实现则为web程序为了处理业务需求的一层代码。业务逻辑层通常调用数据实体与数据交互层，编写业务逻辑，例如用户注册和登录的实际逻辑等；这一层代码通常还管理数据库事务的处理方式，回滚条件等。

Web请求处理层则为web接口地址与对应的handler函数的匹配，handler函数调用业务逻辑实现层的代码，并将结果转化为HTTP结果返回。

此外，项目中还存在一类特殊功能的代码，它们的功能并不属于上述层次，但是也起到相当大的作用。例如

\begin{itemize}
	\item 应用发送邮件的邮件模板代码，则需要单独编写；
	\item 应用中需要使用\textit{面向切片的编程(Aspect-oriented programming, AOP)}\cite{kiczales1997aspect,elrad2001aspect,kiczales1996aspect}部分，这部分代码提供一些辅助功能，例如在调用写数据库和的数据库之前将数据源切换成合适的数据源等；
	\item 应用中需要执行的一些定时调度任务，
\end{itemize}
这部分代码功能特殊，单独作为一层。图\ref{fig:codedesign}展示了代码各层的结构和调用关系，箭头头部一端所指的为被使用或被调用的部分，此图中并不包含Spring容器以及其他层框架所代表的代码部分，且并不表示调用顺序与数据流向。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/codedesign.png}
	\caption{代码分层及各层之间的调用关系，箭头头部一端所指的为被使用或被调用的部分}
	\label{fig:codedesign}
\end{figure}

\subsection{前端代码的设计}

Takkyu项目的前端使用的是Vue框架，并使用\textit{Quasar}\footnote{https://quasar.dev/}作为前端UI框架，而Quasar提供了一套建议(非强制)的代码框架，即将代码分为静态资源、API服务、状态管理、路由管理、组建、页面以及布局层。如清单\ref{code:frontendlayers}

\begin{code}
	\inputminted{text}{./code/frontendcodelayers.txt}
	\caption{前端代码的一个参考结构}
	\label{code:frontendlayers}
\end{code}

\section{接口设计}

\textit{RESTful API}\cite{richardson2008restful,schreier2011modeling}规范是目前最流行的一种API设计规范，其核心思想是将web请求每一个\textit{URI(Uniform Resource Identifier)}\cite{masinter2005uniform}看作资源，服务端与客户端之间传递资源的表现层，客户端使用四个HTTP动词
\begin{itemize}
	\item GET——从服务器中读走一项获多项资源；
	\item POST——在服务器添加一个资源；
	\item PUT——客户端提供完整的新资源以更新服务端的资源；
	\item DELETE——在服务器端删除资源，
\end{itemize}
来对服务器端的资源进行操作，以此实现Representational state transfer。

RESTful API规范是一种很好的接口API设计规范，它具有如下优点：

\begin{itemize}
	\item 高效利用资源——充分利用网络带宽，使得web接口尽量地轻量，仅提供所需的资源；
	\item 简单——REST使用标准的HTTP从而易于理解和使用；
	\item 更好的可扩展性——REST的结果可以被缓存起来以实现高性能
\end{itemize}

不过REST也具有一定的缺点，本研究在实现过程中感受到的一个缺点就是，REST简化了后端的接口设计难度，但是难以应对特殊的字段需求或者复杂的资源请求，例如，业务需要最近一个月的新提问，那么后端需要提供一个允许GET方法的接口，并命名为类似"\textit{/questions-within-recent-month}"，并进行实现，但是这样的接口名字显然存在不够普遍性和不够优雅等缺点。在这样的业务要求下，其实更多的逻辑操作从后端被转移到了前端，也就是在前端编写逻辑从后端现有的接口中进行查询，这样的处理模式虽然使得后端接口简洁和规范了，但是也增加了前端的开发难度。本研究认为，对于一些具有普遍性但很复杂的逻辑，在后端实现是一种更好的办法，而前端应将其精力更多地花费在数据展示上。

本研究在实现过程中，部分采用了REST API设计规范，但仍然保留了一部分并非严格遵守REST规范的逻辑接口。

\chapter{项目的实现与优化}

本节将根据前面的需求与设计，对Takkyu进行一个初步的实现。实现过程大致分为：

\begin{enumerate}
	\item 搭建基础环境——包括安装数据库、缓存以及消息队列等服务的合适版本；
	\item 建立项目，明确项目结构；
	\item 配置项目与其他服务的连接；
	\item 编写基础设施代码，例如项目中可能会使用的方便的注解和切片(即AOP当中的advice)等；
	\item 编写数据库业务，即基本的CRUD等；
	\item 编写非数据库业务，例如邮件服务等；
	\item 编写web层代码，并进行所需的web安全配置，例如跨域请求等设置，并建立API文档用于前后端的交互；
	\item 对重要函数与接口进行测试。
\end{enumerate}

\section{实现过程}

\subsection{搭建数据库}
本研究使用的是MySQL 8.0.23版本，安装在本地计算机，使用默认端口3306。

按照本研究的数据建模，本研究所使用的数据库引擎为InnoDB引擎，使用的索引都是B树索引。


\subsection{搭建Redis环境}
本研究使用Redis 6.2.3版本，安装在本地计算机，使用默认端口6379，并默认绑定只允许本地访问。若取消绑定本地访问，则需绑定特定的访问IP，并且是这认证密码。

\subsection{搭建RabbitMQ环境}

本研究使用Rabbit 3.8.16版本，安装在本地计算机，使用默认端口5672。

\subsection{建立项目}

Spring提供\textit{Spring initializr}\footnote{https://start.spring.io/}工具用于快速建立Spring Boot项目。在Spring initializr中，我们选择项目的编程语言为Java，构建工具为Maven，并选择Spring Boot的版本为2.4.0。添加合适的依赖，即可创建项目。前面提到，由于Takkyu项目暂时并不复杂，所以我们暂时不实用微服务架构；但是为了保持项目结构的清晰性，本研究将原始的Spring Boot项目的结构调整为Java多模块结构，并创建新的Java模块\textit{takkyu-system}。清单\ref{code:takkyusystem}展示了新建的takkyu-system模块的主要项目结构。

\begin{code}
	\inputminted[breaklines]{text}{./code/takkyusystem.txt}
	\caption{takkyu-system模块的主要项目结构}
	\label{code:takkyusystem}
\end{code}

其中src目录存放项目的代码，包括程序代码和部分配置文件和其他静态资源；test目录存放单元测试代码。在\textit{com.takkyu.system}包中\textit{TakkyuApplication.java}是Spring Boot项目的启动类，同目录下存放其他功能代码的Java包。

\subsection{项目配置}

项目配置通常需要提供服务的IP、端口号、用户名以及密码等信息，而这些信息大部分并不适合写在代码里。Spring Boot提供了.properties和.yml(yaml)两种格式的配置文件，用来帮助开发者配置服务。本研究使用yaml的方式进行配置。在Spring Boot中，我们可以同时创建多个配置文件，例如application-dev.yml，application-prod.yml以及application-test.yml分别表示开发模式的配置，生产模式的配置以及测试模式的配置，再结合\textit{@ActiveProfiles}注解自由使用不同配置文件。

通常情况下，数据库密码不应明文填写在配置文件里，否则可能导致数据泄漏。因此，可将密码进行编码，并配置运行时解码，从而降低了数据库泄漏等风险。

以Redis为例，清单\ref{code:redisconfig}展示了Redis的配置情况。

\begin{code}
	\inputminted[breaklines]{yaml}{./code/redisconfig.yml}
	\caption{Redis服务的配置}
	\label{code:redisconfig}
\end{code}
这里我们配置了Redis服务运行的IP地址为127.0.0.1，即为localhost；Redis默认提供了16个数据库，这里使用database 1；由于未开放公网访问权限，所以没有设置Redis密码认证；端口配置为默认端口6379；lettuce是一种Redis的Java客户端，这里配置其连接池参数；最后配置连接超时时间。

在配置文件中参照文档配置好基本的参数后，Spring Boot会自动根据配置文件中提供的参数，创建默认的\textit{RedisConnectionFactory} bean，这个bean可以在项目中直接使用。

Redis存储的内容为字符串，在实际开发场景中，通常我们需要将Java对象存储到Redis以及将Redis中存储的值读取为Java对象。实现这一转换的最佳方式为序列化与反序列化技术\cite{asad2017serialization}。

为此我们采用Java配置的方式配置\textit{Redistemplate} Bean，如清单\ref{code:redisconfigjava}所示：

\begin{code}
	\inputminted[breaklines]{java}{./code/redisconfig.java}
	\caption{RedisTemplate的Java配置方式}
	\label{code:redisconfigjava}
\end{code}

代码中我们使用了FastJsonRedisSerializer即为我们担负配置的序列化器，如清单\ref{code:fastjsonredisserializerjava}所示。

\begin{code}
	\inputminted[breaklines]{java}{./code/fastjsonredisserializer.java}
	\caption{FastJsonRedisSerializer的配置}
	\label{code:fastjsonredisserializerjava}
\end{code}

这样我们就配置好了Redis的基础设施，因此可以在项目中使用我们配置好的RedisTemplate进行Redis的操作。如清单\ref{code:redisservicejava}所示，使用Spring注解自动绑定依赖即可。

\begin{code}
	\inputminted[breaklines]{java}{./code/redisservice.java}
	\caption{RedisService的示例代码}
	\label{code:redisservicejava}
\end{code}

其他服务的配置和Redis服务的配置是类似的，简单的配置可以通过编写配置问题提供默认的Java Bean，复杂的配置可以结合使用Java配置类和配置文件等方式完成。

\subsection{编写数据库业务}

\subsubsection{编写数据库实体类}
首先根据数据库表建立相应的Java实体类，以Comment表为例，编写实体类如清单\ref{code:commentjava}所示。代码中的\textit{@TableName}、\textit{@TableId}和\textit{@TableField}为MyBatis Plus框架提供的注解，用于标识Java类的属性与数据库表中字段的名称之间的对应。当然，MyBatis Plus同样需要进行配置，例如，配置Java代码中驼峰命名和数据库中的小写字母加下划线命名之间的转换等。代码中的\textit{@Data}、\textit{@AllArgsConstructor}和\textit{@Accessors}注解均来自于\textit{Lombok}插件，该插件提供运行时生成Java类的get/set方法以及构造函数等功能，以此简化代码并提高开发效率。
\begin{code}
	\inputminted[breaklines]{java}{./code/comment.java}
	\caption{Comment表对应的实体类Comment.java}
	\label{code:commentjava}
\end{code}

\subsubsection{编写数据库操作}

在有了数据库实体类之后，我们需要在此基础上编写数据库操作才能与数据库进行交互。在Java语言中最基本的数据库交互是使用JDBC(Java Database Connectivity)\cite{fisher2003jdbc,patel1997java}来管理数据库的连接和SQL语句的执行。但是直接食用底层JDBC给项目中带来非常多的重复模板代码，并且需要手动转化Java对象与数据库表字段，导致项目非常繁杂和难以维护。为此我们使用ORM框架Mybatis以及MyBatis Plus插件来完成数据库的交互。MyBatis Plus对MyBatis提供了很多的优化和封装了很多基础查询和复杂查询的实现方式，尽可能减少了配置XML Mapper的方式来书写SQL语句。在使用了MyBatis Plus之后，我们只需继承框架提供的泛型接口，而不用编写额外的实现函数就可以使用数据库的基本CRUD。以Comment类为例。清单\ref{code:commentmapper}即为Comment类与Comment表所对应的Mapper接口的完整代码。

\begin{code}
	\inputminted[breaklines]{java}{./code/commentmapper.java}
	\caption{Comment类与Comment表所对应的Mapper接口}
	\label{code:commentmapper}
\end{code}

MyBatis Plus提供了强大的条件构造器和链式条件构造器用于在Java代码中灵活生成SQL语句，为数据库操作带来了极大的便利。

\subsubsection{编写数据库业务}

通常数据库的业务是在业务层通过调用数据库Mapper进行编写的，同时完成其他非数据库的业务逻辑。清单\ref{code:commentservice}展示了CommentService的部分实现过程。

\begin{code}
	\inputminted[breaklines]{java}{./code/commentservice.java}
	\caption{CommentService的部分实现}
	\label{code:commentservice}
\end{code}

这里CommentService依赖于评论、回答以及回复三个数据库Mapper，通过注解告知Spring Boot通过构造函数在CommentService执行构造函数之时诸如所以来的Bean。其他业务代码与CommentService类似。

\subsection{编写非数据库业务实现}

非数据库业务包含许多方面，例如，缓存的使用、消息队列的使用、邮件服务的使以及web安全配置等。

\subsubsection{Redis在Takkyu中的基本使用}

Redis在Takkyu项目中可以用来缓存热点数据，但除了用作缓存，Redis还能用与管理用户信息。

由于HTTP本身是无状态的，因此所有的请求也是无状态的。在web应用中，服务器通常需要知道用户的某些“状态”，例如用户是否认证、用户是否具有某些权限以及用户是否保存了某些临时信息等。传统的基于session的认证模式\cite{prabhu2015authentication}即用于解决HTTP不管理请求状态的缺点。在基于session的认证模式中，服务器将为每一个登录的用户创建一个session id用于唯一识别此用户以及管理其状态。Session id保存在用户浏览器的cookie中，当用户保持登录状态时，用户发送的每一个请求都将携带浏览器所存储的cookie，服务器接到请求后可以根据存储与cookie中的session id识别用户身份并以此为根据处理用户请求，最后返回响应，如图\ref{fig:sessionbaesdauthentication}所示。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/sessionbaesdauthentication.png}
	\caption{基于session的认证模式}
	\label{fig:sessionbaesdauthentication}
\end{figure}

基于session的认证方式存在一些问题：

\begin{itemize}
	\item 扩展性——session存储在服务端的内存中，服务端内存需要承载成千上万的用户的session并进行管理，系统压力过大；
	\item 多设备存储——基于session的认证方式收到cookie跨域问题的限制，对于移动设备如手机和平板设备发送请求将会引发问题，
\end{itemize}

许多网站采用基于JWT(JSON Web Token)\cite{haekal2016token,jones2011emerging}的认证方式来规避基于session认证方式的缺点。在基于JWT的认证模式中，服务端使用自定义的密钥创建JWT
并返回给客户端，客户端可以将其存储于浏览器的本地存储(Local storage)中，客户端发送每一个请求都将JWT附在HTTP的报头，服务器可以因此可以识别客户端所携带的JWT并作出响应。图\ref{fig:jwtauthentication}为基于JWT认证模式的示意图。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/jwtauthentication.png}
	\caption{基于JWT的认证模式}
	\label{fig:jwtauthentication}
\end{figure}

基于JWT的认证模式解决了服务端负载个扩展性的问题\cite{balaj2017token}，因为JWT是存储于客户端而非服务端，因此减小了服务端的压力，提高了性能；此外JWT认证方式不存在多设备的问题，允许多点登陆。因此，本研究也将采用基于JWT的认证模式。

回到Redis的使用情况，在Takkyu中，我们将使用Redis存储用户的JWT，并用户管理用户的登录状态：当用户登录成功时，使用密钥创建JWT并将其存储于Redis中，并使用此JWT作为key，用户的Java实体对象作为value。当用户发来请求时，服务器从Redis中查找此JWT，若查找到则说明用户已经认证登录，否则认证失败；当用户登出或系统强制下线用户时即将此JWT对应的key从Redis中删除。由于Redis可以多点部署并支持主从复制等特点，既减轻了服务端的负载，也拥有更强的稳定性。

\subsubsection{消息队列在Takkyu中的基本使用}

使用Spring Boot集成RabbitMQ之后，并不需要手动使用命令创建RabbitMQ的交换机和队列。清单\ref{code:rabbitmqconfig}展示了配置文件中的RabbitMQ所需的配置。这里我们提供了RabbitMQ服务的IP为localhost，指定端口为默认端口5672，用户名和密码使用默认值，均为"guest"，并指定消息队列的确认返回模式。

\begin{code}
	\inputminted{yaml}{./code/rabbitmqconfig.yml}
	\caption{使用Spring Boot配置文件配置RabbitMQ的基本信息}
	\label{code:rabbitmqconfig}
\end{code}

我们仍需要在Java配置类中配置所需要的RabbitMQ中的交换机与队列。清单\ref{code:rabbitmqconfigjava}展示了RabbitMQ的Java配置类。在此类中，我们创建了默认交换机名称和由于发送邮件的队列名称，配置了对应的Java bean并将邮件队列与默认直连交换机进行绑定。在配置过程中设置参数使交换机和队列都具有持久性且当消息为空时不自动删除队列。

\begin{code}
	\inputminted[breaklines]{java}{./code/rabbitmqconfig.java}
	\caption{RabbitMQ的Java配置类，配置发送邮件的交换机与队列，\\若RabbitMQ中不存在所配置的交换即与队列则创建一个新的}
	\label{code:rabbitmqconfigjava}
\end{code}

所谓的交换机(Exchange)是AMQP协议中消息被送达的地方，交换机接收消息并将其路由到一个或多个队列，路由的算法取决于交换机类型以及绑定规则。AMQP提供了四种交换机类型：

\begin{enumerate}
	\item Direct exchange;
	\item Fanout exchange;
	\item Topic excahnge;
	\item Headers exchange;
\end{enumerate}
此外，交换机还有一些属性值，例如

\begin{itemize}
	\item 名称(Name)；
	\item 持久性(Durability)——当消息队列重启时，交换机依然存在；
	\item 自动删除(Auto-delete)——当最后一个队列与之解除绑定时交换机被删除；
\end{itemize}
这里使用的直连交换机是基于routing key决定将消息发送到对应的队列。其处理过程是

\begin{enumerate}
	\item 队列通过routing key，例如$K$，绑定交换机，
	\item 当新的消息伴随着routing key $R$到达直连交换机时，交换机将其发送到使得$K = R$的队列。
\end{enumerate}
图\ref{fig:directexchange}为RabbitMQ的直连交换机的工作示意图。在清单\ref{code:rabbitmqconfigjava}中，我们使用队列的名字作为队列的routing key。
\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/directexchange.png}
	\caption{RabbitMQ的直连交换机使用routing key将消息发送到对应的队列}
	\label{fig:directexchange}
\end{figure}

有了交换机和队列之后，我们就能在业务中向RabbitMQ发送消息，例如，在代码清单\ref{code:userservicejava}中，我们使用RabbitTemplate向我们配置的直连交换机发送消息并指定routing key为邮件队列名称。

\begin{code}
	\inputminted[breaklines]{java}{./code/userservice.java}
	\caption{使用RabbitTemplate向我们配置的直连交换机发送消息并指定routing key为邮件队列名称}
	\label{code:userservicejava}
\end{code}

在发送消息之后，我们需要告诉程序如何消费消息。Spring Boot提供了\textit{@RabbitListener}和\textit{@RabbitHandler}注解，开发者提供所需监听的队列并实现处理函数即可，如清单\ref{code:maillistenerjava}所示，我们可以根据传递参数的类型实现多个@RabbitHandler函数。

\begin{code}
	\inputminted[breaklines]{java}{./code/maillistener.java}
	\caption{邮箱队列的监听类}
	\label{code:maillistenerjava}
\end{code}

最后我们来看发送邮件服务的实现。首先我们准备好待发送邮件的模板，如清单\ref{code:registerhtml}所示。其中我们使用带有"th:"标记的HTML标签，这些值最终将有模版引擎\textit{Thymeleaf}渲染成普通的HTML文档。

\begin{code}
	\inputminted[breaklines]{html}{./code/register.html}
	\caption{用户注册时的验证邮件模板}
	\label{code:registerhtml}
\end{code}

最后我们用Thymeleaf将数据发送给模板，并使用使用JavaMailSender发送邮件。
\begin{code}
	\inputminted[breaklines]{java}{./code/emailsender.java}
	\caption{使用JavaMailSender发送邮件，并使用Thymeleaf将数据发送给模版}
	\label{code:emailsenderjava}
\end{code}

这里使用\textit{@Value}注解将配置文件中的一个配置值注入到Java类中的一个属性中。这样我们就完成了程序异步发送邮件的实现，在后面的开发中，还会加入不同种类的异步任务，都可以参照此例进行编写。

\subsubsection{Web服务的实现}

前面提到，本研究将采用RESTful API规范来编写，但为了降低前端项目的逻辑复杂性，本研究保留了一部分接口并非遵循RESTful API规范。例如清单\ref{code:commentcontrollerjava}展示了CommentController类的实现。注意到，我们在类名上加了\textit{@RestController}注解，该注解相当于\textit{@Controller}注解与\textit{@ResponseBody}注解一起使用，用在Java类型上，即等同于在此类型的每一个方法上加@ResponseBody注解。@ResponseBody注解告诉controller将其handler函数的返回值自动序列化为JSON对象并返回给\textit{HttpResponse}对象。

\begin{code}
	\inputminted[breaklines]{java}{./code/commentcontroller.java}
	\caption{CommentController的实现}
	\label{code:commentcontrollerjava}
\end{code}

注意到清单\ref{code:commentcontrollerjava}中的\textit{@Api}和\textit{@ApiOperation}注解，这两个注解来自Swagger API框架\footnote{https://swagger.io/}，其作用是自动生成后端接口文档，包括接口名、支持的HTTP方法、版本、接口参数类型与字段以及接口含义等等。Swagger API框架帮助开发者节省了人工书写后端接口文档的时间和精力，合理清晰的文档为前后端联调开发也提供了方便。

在接口正常返回的情况下，应用将返回相应的数据，并携带HTTP状态码为200。但有时候接口所调用的服务不能正常处理数据，而是抛出各种各样的异常，例如，用户登录时所输入的用户名和密码并不匹配，那么登录服务抛出\textit{UsernameNotFoundException}。一个服务可能抛出若干的不同类型或者同类型但不同参数的异常，如果在Controller里一个一个捕获并根据情况返回相应的错误信息和HTTP状态码，那么Controller代码将几乎丧失可读性，给开发带来麻烦，且难以维护。Spring提供的\textit{@RestControllerAdvice}注解利用AOP的特性，可将服务抛出的异常统一处理。例如，对于用户登录密码错误，我们自定义一个\textit{RuntimeException}的子类，如清单\ref{code:wrongpasswordexceptionjava}所示。我们可以根据场景和需要，定义所需的异常名。

\begin{code}
	\inputminted[breaklines]{java}{./code/wrongpasswordexception.java}
	\caption{自定义一个WrongPasswordExceptionn}
	\label{code:wrongpasswordexceptionjava}
\end{code}
有了相应的自定义异常类之后，我们创建一个全局异常处理类\textit{GlobalExceptionHandler}，如清单\ref{code:globalexceptionhandlerjava}所示。我们对每一个异常定义一处理函数，使用\textit{@ExceptionHandler}注解标记，并使用\textit{@ResponseStatus}注解指定返回Response的状态码。这样，当Controller中抛出未处理的异常时，Spring将使用GlobalExceptionHandler中定义的处理函数统一进行处理，并返回Response对象。


\begin{code}
	\inputminted[breaklines]{java}{./code/globalexceptionhandler.java}
	\caption{全局异常处理类}
	\label{code:globalexceptionhandlerjava}
\end{code}

\subsubsection{网络安全相关配置}

我们并不希望所有人都能任意访问我们的任意接口。例如，对于未登录的用户，我们可以只允许其访问登录和注册相关的接口，若用户在未登录的状态下访问需要鉴权的接口，则返回HTTP 401错误，告知用户需要登录鉴权。

\textit{Spring Security}\footnote{https://spring.io/projects/spring-security}和\textit{Apache Shiro}\footnote{https://shiro.apache.org/}是Java语言中两个流行的web安全框架，它们提供网站认证(Authentication)、鉴权(Authorization)、加密以及session管理等服务。相比之下，Spring Security功能更加丰富，对Spring的集成支持更好。Spring Security提供了以下功能：

\begin{enumerate}
	\item 对于认证和鉴权完善的、具有扩展性的支持；
	\item 对于session fixation、点击劫持(clickjacking)、跨站点访问欺诈(Cross Site Request Forgery, CSRF)等网络安全问题的保护；
	\item Servlet API集成；
	\item 对于Spring MVC的集成
\end{enumerate}

清单\ref{code:websecurityconfigjava}展示了使用Spring Security配置web安全的方法。我们创建\textit{WebSecurityConfig}类并继承Spring Security提供的\textit{WebSecurityConfigurerAdapter}类，重载其方法即可完成配置。在\textit{configure}函数中，我们是使用默认的CORS(Cross-Origin Resource Sharing)配置，并指定了"\textit{/auth}"前缀下的接口均为所有人可访问，其中带"/auth"前缀的接口为Takkyu项目的认证接口，包括登录、登出、注册以及发送验证码等；其余所有接口均要求认证之后才能访问。


\begin{code}
	\inputminted[breaklines]{java}{./code/websecurityconfig.java}
	\caption{网络安全的配置}
	\label{code:websecurityconfigjava}
\end{code}

Spring Security是通过一层一层的Servlet过滤器(Servlet filter)实现功能的，众多的Filter按照一定顺序组成Spring Security的过滤链(Filter chain)，如图\ref{fig:filterchain}\footnote{https://docs.spring.io/spring-security/site/docs/5.4.6/reference/html5/\#servlet-architecture}所示。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.5]{./figures/filterchain.png}
	\caption{Spring Security的Filter Chain}
	\label{fig:filterchain}
\end{figure}

客户端发送到服务端的请求需通过所有的过滤器才能到达Spring MVC的Dispatcher Servlet，并被其转发到相应的Controller，如图\ref{fig:dispatcherservlet}\footnote{https://docs.spring.io/spring-framework/docs/3.0.0.M4/spring-framework-reference/html/ch15s02.html}所示。客户端发送的请求也可能在其中一个过滤器中被拦截，从而未到达Controller。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.5]{./figures/dispatcherservlet.png}
	\caption{DispatcherServlet的工作示意图}
	\label{fig:dispatcherservlet}
\end{figure}


前面提到，本研究使用JWT认证方式。我们首先配置JWT相关信息，如清单\ref{code:jwtyml}所示，客户端发送的请求报头中"Authorization"作为key，"Bearer "加token作为value；我们还指定了生成JWT所用的密钥以及token过期时间。

\begin{code}
	\inputminted[breaklines]{yaml}{./code/jwt.yml}
	\caption{在配置文件中配置JWT参数}
	\label{code:jwtyml}
\end{code}

接着我们编写JWT服务，其提供生成JWT、解析JWT以及验证JWT等功能。清单\ref{code:jwtservicejava}展示了其部分实现。
\begin{code}
	\inputminted[breaklines]{java}{./code/jwtservice.java}
	\caption{JWT服务的部分实现}
	\label{code:jwtservicejava}
\end{code}

使用JWT方式认证的方法是，当客户端使用HTTP GET方法访问"/auth/get"接口时，登录服务正常情况下生成JWT并存入Redis，将生成的JWT放在Response中返回给客户端；客户端将“Bearer " + token作为value放入HTTP的"Authorization"报头中，再次发送请求，请求需要鉴权接口时，首先被过滤器拦截，过滤器解析出报头中携带的JWT并从Redis中查找是否存在此token，若存在，则说明用户登录成功，此时将Spring Security的Authentication对象放入SecurityContext中即可，过滤器将请求交给下一个过滤器处理；若Redis中不存在该token，则登录失败，返回错误信息。清单\ref{code:jwtfilterjava}展示了JWT过滤的实现，此过滤器继承了Spring Scurity提供的\textit{OncePerRequestFilter}类，意味着需要鉴权的接口在每一次被请求见都需要经过此过滤器。在清单\ref{code:websecurityconfigjava}中我们已经将其放入了Spring Scurity提供的\textit{UsernamePasswordAuthenticationFilter}之前。

\begin{code}
	\inputminted[breaklines]{java}{./code/jwtfilter.java}
	\caption{Jwt过滤器的实现}
	\label{code:jwtfilterjava}
\end{code}

这样，基于JWT的认证模式就实现了。到此，我们已经完成了后端程序的基本实现，其他功能都可以参照以上功能来具体实现。这里不再详细叙述。

\subsection{前端实现}

前面讲到，后端使用了Swagger最为API文档框架，Swagger的配置与使用十分简单，只需配置好基本的描述信息和所需生成接口的信息，并在需要生成接口文档的接口上使用\textit{@ApiOperation}对接口信息进行描述即可，当服务器运行时，访问"/swagger-ui/index.html"(对于老版本的Swagger，此接口应为"/swagger-ui.html")就可以看到接口的文档信息，包括所允许的HTTP方法、接口意义以及接口参数的详细信息等，并且提供调试功能。图\ref{fig:swaggerui}展示了Swagger UI界面以及CommetController下的部分接口文档。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.3]{./figures/swaggerui.png}
	\caption{Swagger UI界面以及CommetController下的部分接口文档}
	\label{fig:swaggerui}
\end{figure}

有了接口文档之后，我们便能方便地编写前端项目。本研究使用Quasar作为UI框架，Quasar是一款基于Vue的UI框架，提供了丰富的组件库以及页面布局插件等工具，此外，Quasar还提供了自己的一套CLI命令行工具，可以用来方便地搭建项目、安装依赖、调试项目以及部署项目。在创建项目时，我们使用webpack作为JavaScript的模块打包工具，其能够分析项目结构，识别JavaScript模块以及一些浏览器不能运行的拓展语言，如SCSS等预处理器等，并将其转换以及打包成合适的格式，能够正确地被浏览器运行。使用Quasar创建项目只需要一行命令即可：

\begin{minted}{bash}
    quasar create takkyu-web
\end{minted}
执行过程中选择需要安装的插件，也可以在项目创建之后安装。
清单\ref{code:frontendlayers}展示了项目的结构，其中

\begin{itemize}
	\item apis文件夹存放项目的API调用函数脚本;
	\item assets存放动态资源;
	\item boot存放项目的初始化代码
	\item css文件夹存放格式配置文件;
	\item components文件夹存放所有的基础组件;
	\item pages存放页面组件;
	\item layouts存放页面布局组件;
	\item router存放路由配置；
	\item store存放状态管理配置；
	\item utils存放实用工具函数
\end{itemize}
等。

\subsubsection{编写API函数}

首先编写请求和响应的拦截器，使之在发送请求前先将JWT方法报头，在收到响应之后针对不同的HTTP状态码作出对应行为。清单\ref{code:axiosjs}给出了一个拦截器示例。

\begin{code}
	\inputminted[breaklines]{javascript}{./code/axios.js}
	\caption{拦截器示例代码}
	\label{code:axiosjs}
\end{code}

我们可以在apis目录下编写请求后端接口的函数，例如，清单\ref{code:commentjs}展示了comment接口的请求函数。

\begin{code}
	\inputminted[breaklines]{javascript}{./code/comment.js}
	\caption{comment接口的请求函数}
	\label{code:commentjs}	
\end{code}

接着我们需要编写状态管理，\textit{Vuex}提供了将state按照不同类型分成不同的模块，最后集合起来的状态管理模式。对于Takkyu项目，我们目前只有用户一个模块，清单\ref{code:storeindexjs}和\ref{code:userstatejs}分别展示了全局状态管理对象和用户状态对象，用户状态的获取和更新是通过Vue store的getters, mutations和actions函数来实现，这里均由单独的文件编写。

\begin{code}
	\inputminted[breaklines]{javascript}{./code/storeindex.js}
	\caption{全局状态管理对象}
	\label{code:storeindexjs}
\end{code}

\begin{code}
	\inputminted[breaklines]{javascript}{./code/userstate.js}
	\caption{用户状态的属性，其getters, mutations和actions均由单独的文件编写}
	\label{code:userstatejs}
\end{code}

\subsubsection{编写组件和页面}

组件(Component)为可复用的Vue实例，用于扩展基本的HTML元素以及封装可复用的代码。组件拥有生命周期，可以定制化，并能通过路由在页面中灵活地展示。我们以Answer组件为例，如清单\ref{code:answervue}所示，注意其中的Comment组件也是自定义的。这里编写的Answer组件可以放入到Question组件中，而Question组件又可以放到页面组件中，页面组件又可以放入到页面布局组件中。

\begin{code}
	\inputminted[breaklines]{html}{./code/answer.vue}
	\caption{Answer组件的示例代码}
	\label{code:answervue}
\end{code}

\subsubsection{编写路由}

使用Vue Router可以方便地控制路由及其跳转，如清单\ref{code:routersjs}所示，配置好路由对象之后，在组件的脚本代码里也可以使用注册到Vue对象的Vue Router对象控制路由跳转。

\begin{code}
	\inputminted[breaklines]{javascript}{./code/routers.js}
	\caption{全局router对象的示例代码}
	\label{code:routersjs}
\end{code}

有了状态管理和路由管理，只需要编写组件即可。

\section{后端优化}

前面我们基本实现了后端项目，使得web应用能够访问数据库并进行基本的增删查改，完成网站所需的需求。但是为了更好的系统性能，Takkyu的后端还有很多需要改进和优化的地方。例如，前面提到，单点数据库服务器存在读写频率差距大导致性能降低、事务并发控制压力大、无备份以及存在单点崩溃等诸多问题。当前Takkyu采用的就是单点数据库模式，为了解决上述问题，本研究采用分布式架构对其进行了重构。

\subsection{将数据库从单点模式优化为分布式模式}

\subsubsection{分布式数据库架构的原理}

图\ref{fig:basicmasterslavereplication}展示了一个主库和一个从库的简单分布式数据库架构的工作模式。应用服务将数据写到主库，主库通过主从复制将数据同步到从库，从库只用于读数据，主库通常情况下只用于写数据，只有在特殊情况下用于读取数据。这样，数据库的读和写实现了分离，减轻了单台数据库服务器的事务并发压力和消耗。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.5]{./figures/basicmasterslavereplication.png}
	\caption{一主一从分布式数据库架构的工作模式}
	\label{fig:basicmasterslavereplication}
\end{figure}

主从复制通常是通过在主库记录事务的日志，将日志通过网络复制到从库，从库根据需要执行日志文件中记录的相关事务，从而完成数据的复制。MySQL数据库提供了两种主从复制的实现方式\footnote{https://dev.mysql.com/doc/refman/8.0/en/}，

\begin{enumerate}
	\item 基于二进制日志文件位置的主从复制(Binary log file position based replication)；
	\item 基于全局事务指示器的主从复制(Replication with Global Transaction Identifiers)。
\end{enumerate}

在基于二进制日志文件位置的主从复制模式下，主数据源将数据更新和改变看作“事件”并写入日志文件。根据被记录的数据库的改变，写进二进制日志文件中的信息按照不同的日志格式进行存储，从库按照配置，读取日志文件并执行日志文件中的“事件”，从而完成数据复制。每个从库都保存了日志文件的完整副本，并且从库可以决定日志文件中的哪些语句是需要执行的。所谓基于文件位置进行复制，是指从库跟踪日志文件的文件坐标——文件名和文件中位置，以此标记从库所读到的位置。由于每一个从库都自行跟踪这一坐标，因此不同的从库都可以连接主库并执行不同日志部分，且可以自行决定与主库连接与否。

在基于全局事务指示器(GTIDs)的主从复制模式下，数据库标识并跟踪每一个被提交的事务，在这种模式下，数据库不需要使用GTIDs去指示一个日志文件或者文件内地址。基于GTID的主从复制完全基于事务，若所有在主库提交的事务均在从库被提交，那么主库和从库的事务将保持一致性。GTID被主库和从库保存，当一个被指定了一个GTID的事务在主库被提交(committed)，在该服务器上的之后的所欲具有此GTID的事务都将被忽略，这保证了主库中的每一个事务在从库中最多被执行一次，从而保证了主库于从库的数据一致性。

基于GTID的主从复制是MySQL中的一种新的主从复制模式，相比基于文件位置的复制方式，基于GTID的复制方式不需要找日志文件名和文件内位置，更加利于错误转移，也更加安全和稳定。

此外，MySQL的日志文件提供两种日志格式——基于语句(statement-based)和基于行(row-based)。对于基于语句的日志格式，主库将SQL语句写入二进制日志，从库读取并执行SQL语句从而完成复制；对于基于行的日志格式，主库将单一表的行的改变作为“事件”写入日志中，从库复制这些“事件”到数据库进而完成复制。其中基于行的日志格式是MySQL中默认的日志格式。

本研究采用基于GTID的主从复制模式，并使用基于行的日志格式。

\subsubsection{搭建数据库节点}

本研究只拥有两台电脑资源，其中一台可以通过虚拟机的方式增加一台数据库节点。将这三台数据库节点分别命名为M、S1和S2。其内网IP分别为

\begin{minted}{text}
    M: 192.168.1.18
    S1: 192.168.1.20
    S2: 192.168.1.19
\end{minted}

我们将M配置为主库，S1和S2配置为从库。前面提到，数据库中读操作的次数是大于写操作的的，因此，如果这三台节点中的两台节点设置为主库用于读数据显然并不是很合理。因此，本研究将M设置为主库，将S1和S2设置为从库，如图\ref{fig:masterslave1}所示。S1和S2均从M复制数据，当S1崩溃或者S2崩溃时，主库M和剩下的一个从库仍然能继续工作，从而实现了自动错误转移。但是当主库崩溃时，整个结构便失去了分布式的意义，因为S1与S2之间并没有进行主从复制。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/masterslave1.png}
	\caption{普通的一主二从结构}
	\label{fig:masterslave1}
\end{figure}

图\ref{fig:masterslave1}中的拓扑结构除了当主库崩溃时，已经有效地将读写操作分离，一定程度上解决了单点数据库的问题。但是两个从库直接连接主库，主库需要耗费一定的性能将数据复制到两个数据节点。本研究将上述结构进行了调整，采用图\ref{fig:masterslave2}所示的级联结构。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/masterslave2.png}
	\caption{一主二从的级联结构}
	\label{fig:masterslave2}
\end{figure}

在图\ref{fig:masterslave2}所示的结构中，主库M将数据复制到从库S1，S1将数作为S2的主库把数据复制给S2。这样当从库S2崩溃时，主库M与从库S1仍然形成一主一从的主从结构；而当主库M崩溃时，S1将作为主库，与S2形成主从结构，从而有效地实现了错误转移，相比图\ref{fig:masterslave1}所示的普通三点结构更具弹性。

但是图\ref{fig:masterslave2}所示的拓扑结构仍然有问题，即当S1节点崩溃时，主库的数据无法与从库S2形成主从结构，数据库的分布式架构就被破坏。如果能增加一个节点，构成如图\ref{fig:masterslave3}所示的结构，当S1节点崩溃时，主库和S3节点依然可以形成主从复制结构，这样的拓扑结构中，任意一个节点崩溃时，都能自动进行错误转移。

本研究因为条件限制，暂时无法实现图\ref{fig:masterslave3}所示的数据库分布结构，而是采用了三个节点时相对更好的图\ref{fig:masterslave2}所示的结构。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/masterslave3.png}
	\caption{四个节点的数据库分布结构}
	\label{fig:masterslave3}
\end{figure}


\subsubsection{配置数据库主从关系}

\paragraph{配置主库} 

清单\ref{code:sourcemyconf}展示了主库M的配置。我们配置主库的server\_id为1，并开启GTID模式。MySQL的日志模式默认是开启的，其日志格式也默认为基于行的格式。

\begin{code}
	\inputminted[breaklines]{text}{./code/my.conf}
	\caption{主库的主要配置}
	\label{code:sourcemyconf}
\end{code}

\paragraph{配置从库}

清单\ref{code:slave1myconf}展示了从库节点S1的的主要配置，分配其server\_id为2，开启GTID模式，其中skip\_slave\_start配置为1，即开启手动模式开启主从复制。清单\ref{code:slave2myconf}展示了S2节点的主要配置，与S1类似，其server\_id设置为3。
\begin{code}
	\inputminted[breaklines]{text}{./code/slave1my.conf}
	\caption{数据库节点S1的主要配置}
	\label{code:slave1myconf}
\end{code}

\begin{code}
	\inputminted[breaklines]{text}{./code/slave2my.conf}
	\caption{数据库节点S2的主要配置}
	\label{code:slave2myconf}
\end{code}

\subsubsection{创建专用于数据复制的数据库用户}

主从复制的实现需要在主库中添加一个专用与复制的用户，并限制其权限仅仅为复制数据：

\begin{minted}[breaklines]{bash}
mysql> create user 'repl'@'192.168.1.%' identified by 'password';
mysql> grant replication slave on *.* to 'repl'@'192.168.1.%';
\end{minted}
注意到这里我们仅仅赋予了"replication slave"权限给用户"repl"，并限制其允许访问的IP，这里还可以将其指定为具体的内网IP。同样地，在S1节点也创建一个专用于复制的用户。

\subsubsection{配置主从复制}
清单\ref{code:changesourcesql}展示了在从库S1中将主库M配置为其数据源，注意最后的"master\_auto\_position = 1"为GTID模式下开启的自动定位，而无需手动指定文件位置。从库S2的配置也是类似的。
\begin{code}
	\inputminted[breaklines]{sql}{./code/changesource.sql}
	\caption{在从库中配置数据源}
	\label{code:changesourcesql}
\end{code}

在配置好数据源之后，只需要在从库用命令

\begin{minted}{sql}
mysql> start slave;
\end{minted}
即可手动开启主从复制，使用命令
\begin{minted}{sql}
mysql> show slave status \G;
\end{minted}
可查看主从复制的状态，当看到参数
\begin{minted}{sql}
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
\end{minted}
时，即表明主从复制正常运行中。

\subsubsection{使用MySQL Router作为路由数据源}

现在我们已经有了数据节点，数据节点之间也已经配置好了主从复制，我们需要web应用知道读数据和写数据分别应该使用哪一个数据库。比较直接的方法是，在程序中通过AOP的方式，在每次数据库操作前，根据操作的读写类型，将web应用使用的数据源进行切换。我们现在有三个数据节点，按照这个方法，需要在web应用中配置三个数据源。这样硬编码带来的问题是，当我们要在现有的数据库拓扑中添加新的节点或者删除一个节点时，需要对web应用的代码进行改动；不仅如此，我们还需要通过代码来实现数据库的错误转化，即使用程序判断当前数据库节点是否可用，如果不可用则切换成其他节点等。可见，在程序中来实现数据库的节点分配和错误转换将会导致应用代码与数据库分布架构耦合，每次修改程序也会带来较大的测试和部署成本。

数据库分布架构的具体情况，不应该由应用程序来关心。应用程序只需要知道那个数据源用于写数据，哪个用于读数据即可，而不需要知道数据节点的细节，如IP地址等。为此，本研究使用MySQL官方提供的路由中间件\textit{MySQL Router}\footnote{https://dev.mysql.com/doc/mysql-router/8.0/en/}来配置数据节点拓扑。

我们将MySQL Router安装在本地计算机，按照清单\ref{code:mysqlrouterconf}所示配置本研究中的数据库节点。

\begin{code}
	\inputminted[breaklines]{text}{./code/mysqlrouter.conf}
	\caption{使用MySQL Router配置数据源}
	\label{code:mysqlrouterconf}
\end{code}

在"routing:basic\_failover"一栏中，我们配置用于写数据库的数据源。可以看到，我们配置了数据源IP，分别为数据节点M和S1的IP地址，并设置路由策略为"first-available"，即每次使用第一个可用的节点。这样，当主库M可用的情况，每次写数据都将使用主库M，当其不可用时，根据"first-available"策略，MySQL Router将数据请求路由到从库S1，从而完成错误转移；在"routing:balancing"一栏中，我们配置数据节点S1和S2的节点IP，路由策略配置为"round-robin"，即轮训策略。轮训策略按照一次访问S1，下次访问S2的策略来进行路由，这样可以缓解单个从库的读取压力。此外，我们分别将写库和读库的路由地址绑定为了本地的7001和7001端口，当应用程序要写数据时，访问本地7001端口即可；要读数据则访问本地7002端口，至于具体访问哪个数据节点，则交给MySQL Router取处理。

\subsubsection{配置应用程序}

到此，我们已经准备好了数据库节点和数据路由中间件，只需配置好应用程序，使之访问中间件MySQL Router即可。清单\ref{code:datasourceconfigyml}展示了在Spring Boot的配置文件中配置读写数据源的关键配置。

\begin{code}
	\inputminted[breaklines]{yaml}{./code/datasourceconfig.yml}
	\caption{在Spring Boot的配置文件中配置多数据源}
	\label{code:datasourceconfigyml}
\end{code}

首先我们取消Spring Boot的自动配置数据源。然后在数据源配置中指定MySQL Router所绑定的7001和7001端口。接着新建一个配置类，如清单\ref{code:mybatisplusconfigjava}所示，我们配置了主从两个数据源，并配置一个动态数据源的Java Bean。

\begin{code}
	\inputminted[breaklines]{java}{./code/mybatisplusconfig.java}
	\caption{在配置动态路由数据源}
	\label{code:mybatisplusconfigjava}
\end{code}

前面提到，主库通常只用来写数据，但在一些特殊需求时，也需要用从主库中读数据。考虑到程序中主要使用AOP技术来实现数据源的切换，我们新建一个\textit{@Master}注解，如清单\ref{code:masterjava}所示，因为该注解只用来指明哪些服务需要强制使用主数据库，所以不需要任何属性。

\begin{code}
	\inputminted[breaklines]{java}{./code/master.java}
	\caption{自定义Master注解}
	\label{code:masterjava}
\end{code}

到此，我们已经做好了动态配置路由源的准备。本研究使用Spring AOP来实现动态切换数据源。


如清单\ref{code:datasourceaspectjava}所示。由于Spring的事务管理也是通过AOP来实现的，为了避免事务管理在切换数据源之前进行，这里使用注解来\textit{@Order(1)}为此aspect设置AOP的管理级别，只要其值小于Spring事务管理的值，那么就会优先于事务管理执行此aspect，从而避免事务管理失效。代码中，使用\textit{AspectJ}语言来指示需要执行数据切换的代码点，这里本研究指定了读和写两个切点，其中读切点为，service包中函数名以select和get为开头的所有函数，以及标注了@Master注解的函数，如清单\ref{code:read}和清单\ref{code:write}所示。这样，在这些函数执行前Spring代理将会把数据源切换为我们配置的主数据源。在每次这些切点函数执行结束之后，清空动态数据源。

\begin{code}
	\inputminted[breaklines]{java}{./code/datasourceaspect.java}
	\caption{使用AOP管理数据源的切换}
	\label{code:datasourceaspectjava}
\end{code}

\begin{code}
	\begin{minted}{text}
"!@annotation(com.takkyu.system.annotation.Master) " +
"&& (execution(* com.takkyu.*.service..select*(..)) " +
"|| execution(* com.takkyu.*.service..get*(..)))"
	\end{minted}
	\caption{读切点的表达式}
	\label{code:read}
\end{code}

\begin{code}
	\begin{minted}{text}
"@annotation(com.takkyu.system.annotation.Master) " +
"|| execution(* com.takkyu.*.service..insert*(..)) " +
"|| execution(* com.takkyu.*.service..save*(..)) " +
"|| execution(* com.takkyu.*.service..update*(..)) " +
"|| execution(* com.takkyu.*.service..delete*(..)) " +
"|| execution(* com.takkyu.*.service..remove*(..)) " +
"|| execution(* com.takkyu.*.service..edit*(..))"
	\end{minted}
	\caption{写切点的表达式}
	\label{code:write}
\end{code}

这样，我们便在程序中配置好了不同服务使用的数据源，并解藕了应用程序与实际数据库节点的具体细节。

%TODO: 点赞唯一性实现
\subsection{Redis在Takkyu中的进一步使用}

前面提到了Redis在Takkyu中可以用管理用户登录和存储热点数据。Redis作为多才多艺的缓存数据库，其快速查找能力也可以用来实现一些特殊需求。在本研究中，用户点赞是一个可能会比较频繁的操作，但是一般来说，一个用户应当只能给一个回答点赞一次。为实现这一需求，我们可以在数据库中创建一个up\_vote表，其每一行数据为某一用户给一个回答点赞的记录，当用户看到回答时，服务器查询up\_vote表，看有没有一条记录表面该用户已经为该回答点赞，若有，则前端显示已点赞，并防止用户多次点赞；若没有，则允许用户点赞。

上面的方案确实可行，但是所有的用户为所有的回答点赞的记录随着时间的推移越积越多，数据越多，查询数据库将会拖慢应用性能。Redis提供的集合数据结构正好可以解决这一问题。我们将一条点赞记录的用户id与被点赞回答的id组合起来作为集合的value存入点赞对应的key当中，而Redis的集合提供\textit{SISMEMBER}\footnote{https://redis.io/commands/sismember}函数用于查找集合内是否存在某个值，其复杂度为$O(1)$，性能十分强大。

此外，我们设计将每一条点赞记录根据点赞时间从Redis记录到MySQL表中，对于一周内的点赞记录，我们就将其保存在Redis中，对于一些老数据，例如一周前的点赞记录，我们可以将其转移到数据库中存储，若需要查找该数据记录，当Redis缓存未命中时，才去数据库中查询，这样一来也减轻了Redis部署的服务器的内存压力。为实现这一设计，我们使用Redis提供的有序集合来存储点赞记录。Redis的有序集合除了提供key和value之外，还为每一个value提供了socre属性。我们设计将每一条点记录的时间戳作为score值，并设置定时任务，如清单\ref{code:upvotetaskjava}所示，，每过一周就根据socre值查询Redis中一周内的点赞记录，将其持久化到数据库中。采用Redis的有序集合之后，查找某条点赞记录需要使用\textit{ZSCORE}\footnote{https://redis.io/commands/zscore}函数，而该函数的时间复杂度同样为$O(1)$。

\begin{code}
	\inputminted[breaklines]{java}{./code/upvotetask.java}
	\caption{将点赞记录转移到数据库的定时任务}
	\label{code:upvotetaskjava}
\end{code}

\subsection{消息系统的设计优化}

\subsubsection{优化原理与配置}
同样，我们可以采用如图\ref{fig:notificationentity}所示的数据库表来存储消息数据。

\begin{figure}[H]
	\centering
	\includegraphics[scale=1]{./figures/notificationentity.png}
	\caption{Notification数据建模}
	\label{fig:notificationentity}
\end{figure}

当用户点击进入消息系统时，查询数据库表，获得用户的已读或未读消息，将其返回给前端进行展示。此过程可以通过定时任务进行推送，也可以采用前端轮训。但是对于这种业务，后端能主动将数据推送给前端才能保持消息的实时性。显然，传统的前端访问后端响应的模式并不适合实时消息业务。\textit{WebSocket}\cite{lombardi2015websocket}则是应对这类场景更好的解决方案。

WebSocket是一种通信协议，提供了基于单个TCP连接的全双工通信管道。使用WebSocket连接的两个应用，双方都可以互相发送消息，其工作方式如图\ref{fig:fullduplex}所示。
\begin{figure}[H]
	\centering
	\includegraphics[scale=0.65]{./figures/fullduplex.png}
	\caption{全双工通信的工作方式}
	\label{fig:fullduplex}
\end{figure}

相比于数据库，消息队列对于实时消息业务是一种更好的存储工具。服务端接收到消息将其存储与消息队列，消息队列通过WebSocket再将消息发送给订阅某个队列的用户，从而完成实时消息系统。直接使用WebSocket通信则需要开发者自己定义消息的高级语义，使得连接的两端都能够识别这些语义。为了方便开发，我们将采用面向流式文本的消息协议(Streaming Text Oriented Messaging Protocol, STOMP)作为消息封装协议。STOMP在WebSocket之上定义了一套消息语义，其看上去和HTTP请求结构相似，包含了一个命名、一些报头以及一个数据负载(payload)。前面提到，本研究所使用的消息队列RabbitMQ是支持STOMP协议的，因此，我们可以使用RabbitMQ作为消息代理，如清单\ref{code:websocketconfigjava}所示。

\begin{code}
	\inputminted[breaklines]{java}{./code/websocketconfig.java}
	\caption{配置WebSocket并使用RabbitMQ作为外部STOMP消息代理}
	\label{code:websocketconfigjava}
\end{code}

RabbitMQ提供如下几种消息目的地址类型：

\begin{itemize}
	\item \textit{/exchange}——可发送任意的routing key也可订阅任意的绑定模式；
	\item \textit{/queue}——发送到队列和订阅队列消息由STOMP网关来管理；
	\item \textit{/amq/queue}——发送到和订阅STOMP网关之外创建的队列；
	\item \textit{/topic}——发送和订阅短暂(transient)和持久的(durable)的队列；
	\item \textit{/temp-queue/}——创建临时队列。
\end{itemize}

在清单\ref{code:websocketconfigjava}的\textit{configureMessageBroker}函数中，我们指定了两种消息目的地址类型，分别为/topic和/queue\footnote{https://www.rabbitmq.com/stomp.html}。其中/topic类型用于发布/订阅的用户广播消息，即发布系统通知等消息；而/queue类型则适合用于点对点的普通队列消息，即用于发布“问题被回答”等类的消息。随后，我们在代码中配置了RabbitMQ的服务信息，如端口、用户和密码等；在\textit{registerStompEndpoints}函数中，我们暴露了三个地址前缀，分别用于发布系统通知、评论通知和问题回答通知。

由于我们使用了JWT鉴权方式，因此我们也需要解析JWT获得用户信息。在清单\ref{code:websocketconfigjava}中的\textit{customizeClientInboundChannel}函数中，我们加入了自定义的jwt拦截器，如清单\ref{code:jwtchannelinterceptorjava}所示。

\begin{code}
	\inputminted[breaklines]{java}{./code/jwtchannelinterceptor.java}
	\caption{配置JWTChannelInterceptor，用户获取用户信息}
	\label{code:jwtchannelinterceptorjava}
\end{code}

\subsubsection{业务的编写}

清单\ref{code:notificationservicejava}展示了消息推送服务的部分代码，其中\textit{broadcastAnnouncement}和\textit{sendComment}分别展示了广播系统通知和发送给特定用户的点对点消息的业务方式。

\begin{code}
	\inputminted[breaklines]{java}{./code/notificationservice.java}
	\caption{消息服务的部分代码}
	\label{code:notificationservicejava}
\end{code}

以点对点的推送消息——评论信息为例，如清单\ref{code:notificationcontrollerjava}为例，\textit{handleComment}函数上使用注解\textit{@MessageMapping}意味着此函数处理"/app/comments"目的地址所接收到的消息，函数的第一个参数为前端传递的消息Payload对象，第二个参数为前面配置的JWT拦截器中构建的鉴权对象，我们从中获取发送者的用户名，最后使用清单\ref{code:notificationservicejava}中的服务将其转发到"/queue/comments"地址，Spring会将改地址进一步转换为指代具体接受用户的详细地址"/queue/{username}/comments"，进而转发给用户名为username的用户。

\begin{code}
	\inputminted[breaklines]{java}{./code/notificationcontroller.java}
	\caption{评论信息的推送}
	\label{code:notificationcontrollerjava}
\end{code}

客户端建立WebSocket连接并发送消息如清单\ref{code:handlecommentjs}所示。

\begin{code}
	\inputminted[breaklines]{javascript}{./code/handlecomment.js}
	\caption{客户端发送评论消息的示例}
	\label{code:handlecommentjs}
\end{code}
对于用户订阅(subscribe)推送消息的实现方式是类似的。此外，应用程序依然可以在WebSocket的基础上定时发送推送消息，实现多元化的消息推送系统。

到此，消息系统的优化也就完成了。


\chapter{系统测试与总结}

本章我们将测试当前系统的核心功能、总结系统的优缺点并对系统未来进一步的开发和维护进行规划。

\section{系统测试}

\subsection{基础设施测试}

本项目中基础设施包括MySQL数据库、MSQL Router中间件、Redis缓存数据库、RabbitMQ以及用于发送邮件的三方服务器。

\subsubsection{测试MySQL服和MySQL Router}
对于MySQL服务和MySQL Router中间件，本研究通过向主库插入测试数据，在从库中查询数据来验证服务的有效性。

使用如下命令来验证MySQL Router的7001端口和7002端口能否成功访问三个数据库节点，其中port为端口号，username和password分别为不同数据节点的中使用的用户名和密码。
\begin{code}
	\begin{minted}[breaklines]{bash}
mysql -P{port} -u{username} -p{password} --protocol=TCP -e "select @@hostname"
	\end{minted}
\end{code}

使用以上命令，测试端口7001，并重复许多次，都将得到如下结果
\begin{code}
	\begin{minted}{text}
+---------------+
| @@hostname    |
+---------------+
| hpz-mbp.local |
+---------------+
	\end{minted}
\end{code}
将主数据库服务关闭，再次使用上述命令测试端口7001，结果为
\begin{code}
	\begin{minted}{text}
+------------------+
| @@hostname       |
+------------------+
| hpzubuntuserver1 |
+------------------+
	\end{minted}
\end{code}
可见，当主库宕机或者崩溃是，MySQL Router成功将从库S1作为了主库，实现了错误转移。

使用测试命令测试7002端口，如下两种结果则将轮流出现
\begin{code}
	\begin{minted}{text}
# Result 1
+------------------+
| @@hostname       |
+------------------+
| hpzubuntuserver1 |
+------------------+
# Result 2
+--------------------+
| @@hostname         |
+--------------------+
| hpz-ubuntu-desktop |
+--------------------+
	\end{minted}
\end{code}
由此可见，MySQL Router成功采用了我们配置文件所配置的访问策略。

\subsubsection{测试Redis}

我们使用单元测试来测试Redis服务以及应用程序与Redis交互，如清单\ref{code:redistest}所示。

\begin{code}
	\begin{minted}[breaklines]{java}
@Test
public void redisTest() {
    assertTrue(redisService.set("foo", "foo"));
    assertNotNull(redisService.get("foo"));
    assertTrue(redisService.delete("foo"));
    assertNull(redisService.get("foo"));
}
	\end{minted}
	\caption{Redis服务的一部分单元测试}
	\label{code:redistest}
\end{code}
所有测试都成功通过。

\subsubsection{测试RabbitMQ以及邮件服务}

我们使用Postman向"/auth/activation-code"接口发送请求，模拟用户注册，如图\ref{fig:postactivationcode}所示。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.35]{./figures/testactivationcode.png}
	\caption{在Postman中发送请求}
	\label{fig:postactivationcode}
\end{figure}
如果所提供的邮箱存在的话，那么其回收到如图\ref{fig:emailtest}所示的邮件信息。

\begin{figure}[H]
	\centering
	\includegraphics[scale=0.7]{./figures/emailtest.png}
	\caption{用户收到的验证码信息}
	\label{fig:emailtest}
\end{figure}

可见，系统成功使用RabbitMQ处理了“发送邮件”这一异步消息，并且系统成功将其发送给了用户。

\subsubsection{接口测试}

本研究目前对主要的接口进行了测试，包括以/auth、/question、/answer、/comment、/reply、/user以及/profile为前缀的接口，均成功可用。

\subsubsection{前端及其他测试}

对前端项目进行测试，主要是关于对后端接口的请求和前端页面的展示和跳转的测试。目前后端测试成功的接口在前端调用中均正常运行，前端页面跳转也无异常。系统其他部分，如异常处理、消息发送、定时任务等的测试，我们在一定程度上使用测试数据进行了初步测试。

\section{项目评估与总结}

本研究通过搭建Takkyu项目，以问答网站为载体，对网站的开发做了一定程度的探索。本研究的目标并非要开发一款能够媲美Quora和知乎等知名网站的应用，而是借助问答网站这一形式，对知识社区类网站的设计、架构、技术选型等基础环节进行研究，在此过程中进一步了解和学习当前网站开发的主流编程语言、开发框架、技术架构以及其他流行技术。Takkyu到此除了基本的代码设计与开发，还进行了一定程度的优化处理，因此其相比于一款从设计上满足要求的网站具有更强的扩展性、更好的稳定性以及更高的可用性。

当前系统具有如下优点：

\begin{itemize}
	\item 使用了基于JWT的认证模式，使得应用在认证模块具有较高的扩展性和可用性；
	\item 使用了分布式的数据库架构，使得数据库的读写操作分离开，解决了单点数据库的各种问题；
	\item 使用Redis对系统中非结构数据进行存储，并对热点数据进行缓存，提高了系统的整体性能；
	\item 使用消息队列处理异步任务和消息数据模型，使得应用具有高扩展性；
	\item 前后端分离编写，解藕了前后端的开发，使得后端具有高复用性，前端可拓展性，
\end{itemize}
同时，当前系统的不足之处也不少：

\begin{enumerate}
	\item 网站的分布式数据库架构还有进一步优化的空间；
	\item 缓存数据库Redis暂时没有采用分布式架构进行优化；
	\item 网站的搜索功能不能仅用数据库的模糊匹配搜索，可以使用分布式搜索引擎\textit{Elastic Search}\footnote{https://www.elastic.co/}进一步优化；
	\item 由于本研究目前前端技术有限，并没有开发出一款具有良好使用体验的前端项目；
	\item 没有对系统中的监控模块进行测试和利用；
	\item 项目没有进行部署环节——对于部署的一整套完善流程，本研究并没有进行探索，
\end{enumerate}

这仅仅是Takkyu项目的初始架构，前面提到，任何网站并非在诞生时就是大型网站，其一定还有很多的不足之处。Takkyu在未来的发展中，将不断吸取优秀的开发经验，弥补上述不足。

%最后设置格式，插入参考文献。
\defaultfont
%\bibliographystyle{gb7714-2015}
\clearpage
\phantomsection
\addcontentsline{toc}{chapter}{参考文献}
%\bibliography{document}
%\nocite{*}
\printbibliography
%插入致谢
\chapter*{致 \qquad 谢}
\addcontentsline{toc}{chapter}{致谢}

毕业论文写到这里，我知道我的本科生涯即将结束。在此之际，我想首先感谢我的指导老师——王林聪教授，王教授在我拟定选题时提供了宝贵的建议，并在过程中给予我很大的支持。早在毕业设计之前，王教授在指导我做其他科研训练时就教给我很多经验，并教导我学习要秉持实事求是，不要不懂装懂的态度，我受益匪浅。我想感谢我的室友，感谢他们四年来的陪伴，让我的大学时光不那么孤独，尤其要感谢室友在我实习期间经常提醒我注意毕业设计的进度，否则我可能不能按时完成论文。我想感谢我所有的同学和老师们，感谢他们平时对我的帮助，并让我在大学四年里开拓了眼界，学习到很多经验和知识。感谢丰小月老师，对我的论文结构和内容等方面，提供了不少建议。最后，我要感谢我的父母对我多年的养育，没有他们就没有现在的我！

我很舍不得离开学校，结束这十几年来的求学生涯。但我也应该卸下学生的身份，走向社会，开启人生的新篇章了。衷心祝愿我的指导老师身体健康，科研顺利。祝愿室友们无论是工作还是继续读研深造，都前途光明。祝愿我自己，早日实现自己的理想！
%\chapter*{本科期间发表论文和科研情况}
%\addcontentsline{toc}{chapter}{本科期间发表论文和科研情况}
%\thispagestyle{empty}


\end{document}
